Pattern-Matching Shortcuts in Swift-Testing?

Hi! I have some tests that are using pattern matching to derive some state (and then testing the value of that state). Here is an example:

enum E {
  case a(x: Int, y: Int)
  case b
}

@Test func t1() throws {
  let e = E.a(x: 1, y: 1)
  let (x, y) = try #require(
    {
      switch e {
      case .a(x: let x, y: let y):
        return (x, y)
      default:
        return nil
      }
    }()
  )
  #expect(x == 1)
  #expect(y == 1)
}

@Test func t2() throws {
  let e = E.a(x: 1, y: 1)
  let (x, y) = try #require(
    {
      if case .a(x: let x, y: let y) = e {
        return (x, y)
      }
      return nil
    }()
  )
  #expect(x == 1)
  #expect(y == 1)
}

I want to (first of all) test that my state matches the correct pattern (and throw an error or fail if not). I want to next test that the value of that state is what I expect it to be.

Both of these tests work… but it's a little clunky and bulky and I am wondering if there are any shortcuts or modern language features that might clean these up (either from swift or from swift-testing).

I do have the option to try and roll my own case detection helpers:

extension E {
  var asA: (x: Int, y: Int)? {
    if case .a(x: let x, y: let y) = self {
      return (x, y)
    }
    return nil
  }
}

@Test func t4() throws {
  let e = E.a(x: 1, y: 1)
  
  let (x, y) = try #require(e.asA)
  #expect(x == 1)
  #expect(y == 1)
}

This works… but AFAIK there is no way to get these case-detection helpers automatically from the compiler. There is an option to use something like a TestUtils macro (similar to CaseDetectionMacro)… but I am also wondering and brainstorming what else might be out there in terms of shortcuts for these pattern matching tests. Thanks!

1 Like

case does not currently produce an expression, so there's no shorthand like #expect(case .foo = bar) like you might be looking for. It's simply not syntactically valid Swift.

There's an issue tracking this in the general case (not just about testing) somewhere on the Swift repo, but I can't find it. Feel free to file another!

2 Likes

Hmm… I was thinking maybe one of the shortcuts from SE-0380 might help clean up that code. I see some build failures trying to use an if expression (and switch expression) directly in require:

enum E {
  case a(x: Int, y: Int)
  case b
}

func f(e: E) {
  let _: (x: Int, y: Int)? = if case .a(x: let x, y: let y) = e {
    (x, y)
  } else {
    nil
  }
  let _: (x: Int, y: Int)? = switch e {
  case .a(x: let x, y: let y):
    (x, y)
  default:
    nil
  }
}

@Test func test() throws {
  let e = E.a(x: 1, y: 1)
  
  let _ = try #require(
    if case .a(x: let x, y: let y) = e {
      (x, y)
    } else {
      nil
    }
  )
  
  let _ = try #require(
    switch e {
    case .a(x: let x, y: let y):
      (x, y)
    default:
      nil
    }
  )
}

Here are the errors:

macro expansion #require:1:22: error: 'if' may only be used as expression in return, throw, or as the source of an assignment
`- /Users/rick/Desktop/MyLibrary/Tests/MyLibraryTests/MyLibraryTests.swift:31:4: note: expanded code originates here
29 |       nil
30 |     }
31 |   )
   +--- macro expansion #require ---------------------------------------
   |1 | Testing.__checkValue(if case .a(x: let x, y: let y) = e {
   |  |                      `- error: 'if' may only be used as expression in return, throw, or as the source of an assignment
   |2 |       (x, y)
   |3 |     } else {
   +--------------------------------------------------------------------
32 |   
33 |   let _ = try #require(

/Users/rick/Desktop/MyLibrary/Tests/MyLibraryTests/MyLibraryTests.swift:26:5: error: 'if' may only be used as expression in return, throw, or as the source of an assignment
24 |   
25 |   let _ = try #require(
26 |     if case .a(x: let x, y: let y) = e {
   |     `- error: 'if' may only be used as expression in return, throw, or as the source of an assignment
27 |       (x, y)
28 |     } else {

macro expansion #require:1:22: error: 'switch' may only be used as expression in return, throw, or as the source of an assignment
`- /Users/rick/Desktop/MyLibrary/Tests/MyLibraryTests/MyLibraryTests.swift:40:4: note: expanded code originates here
38 |       nil
39 |     }
40 |   )
   +--- macro expansion #require ---------------------------------------
   |1 | Testing.__checkValue(switch e {
   |  |                      `- error: 'switch' may only be used as expression in return, throw, or as the source of an assignment
   |2 |     case .a(x: let x, y: let y):
   |3 |       (x, y)
   +--------------------------------------------------------------------
41 | }
42 | 

/Users/rick/Desktop/MyLibrary/Tests/MyLibraryTests/MyLibraryTests.swift:34:5: error: 'switch' may only be used as expression in return, throw, or as the source of an assignment
32 |   
33 |   let _ = try #require(
34 |     switch e {
   |     `- error: 'switch' may only be used as expression in return, throw, or as the source of an assignment
35 |     case .a(x: let x, y: let y):
36 |       (x, y)

I'm not completely sure I understand why those expressions would fail to compile in require. I'm not blocked on this… but it would be a neat shortcut if it worked.

1 Like

It's not specifically related to #require or Swift Testing or macros. It's a general limitation that if and switch expressions can only be used in certain contexts right now. As the error message says, the allowed contexts are return, throw, and variable assignments.

Allowing them everywhere was considered for SE-0380 but excluded (for now) because it would have led to "some source breaking edge cases". This is discussed in more detail in the Future Directions section of SE-0380.


I suppose you could also change your code to something like this to get rid of the immediately invoked closure expression:

let optionalTuple: (Int, Int)? = switch e {
case .a(x: let x, y: let y):
    (x, y)
default:
    nil
}
let (x, y) = try #require(optionalTuple)

(Unfortunately, the explicit type annotation for optionalTuple is required.)

1 Like