Shorthand for multiple #expect in Swift Testing

A fairly common pattern in the tests I've seen/written are to have multiple expects at the end of a test or after making a change to the system under test like so:

#expect(sut.validate(" ") == "INVALID")
#expect(sut.validate("\n") == "INVALID")
#expect(sut.validate("") == "EMPTY")

While the #expect with == is nicer that XCTAssertEquals it's still a bit repetitive and noisy.
It would be great if there was a version of the macro that looked like this at the call site. I don't know but it feels like it could just expand to the code snippet above with an #expect on each expression.

#expect {
	sut.validate(" ") == "INVALID"
	sut.validate("\n") == "INVALID"
	sut.validate("") == "EMPTY"
}
#expect(
  sut.validate(" ") == "INVALID" &&
  sut.validate("\n") == "INVALID" &&
  sut.validate("") == "EMPTY"
)

?

1 Like

That works, but the problem with that is that if one fails the output is not nearly as nice

Presumably the benefit of a block-like syntax would be that swift-testing could issue a separate issue for each CodeBlockItem expression (with the correct location) instead of a single issue for the conjunction of all of them.

That being said, I don't think it's particularly desirable to elide #expect in this way. Test code should have a different style that's more sequential and explicit about what's being asserted; folding multiple expectations into a single block may be "less repetitive" but it muddles things by making it less clear what part of the block is an expression being required and what is just other incidental code in the block.

8 Likes

My gut instinct says the block like macro would only accept expression of bools, no statements etc.

The Arrange, Act, Assert pattern is fairly common way of doing unit tests, and that lends itself to having a block of #expect without any other logic in between

I suspect this will resolve that issue: [Mini-Vision] Rethinking expectation capture in Swift Testing

For better or for worse, what you're looking for here is not possible in a clean way with expression macros. The closure will be evaluated before macro expansion occurs, so you'll end up with diagnostics about discarded results from the == operator calls. (Insert hand-waving about result builders here.)

It's also not clear here if && or || should be inferred, and it may not be clear to somebody reading your code which one is intended.

1 Like

issue a separate issue for each CodeBlockItem is that mean
it can allocate memory like an array separated by commas so it enhances readability...

Ah that's good about the rethink helping with the &&. (I don't understand it much though!)

Maybe it's just me but I think && is fairly obvious, in the same way a comma separated list of conditions in a guard or if are &&. It could also be solved with #expectAll, which would allow a counterpart #expectAny.

I don't know the practicalities of macros but I think even spelling it like this would be easier to read for me personally.

#expectAll(
	sut.validate(" ") == "INVALID",
	sut.validate("\n") == "INVALID",
	sut.validate("") == "EMPTY"
)

thank you

@Jon889 I'm not sure if your example above is contrived or not, but it seems like it may be more suitable as a parameterized test function. Then, you would only need one #expect, and you will get granular results and a good Xcode UI experience:

@Test(arguments: [
  (" ", "INVALID"),
  ("\n", "INVALID"),
  ("", "EMPTY"),
])
func example(input: String, expectedOutput: String) {
  #expect(sut.validate(input) == expectedOutput)
}
3 Likes

Yep exactly, I use arguments quite a bit, this example is a bit contrived and reduced so as not to share my companies code.

It would be mostly for checking multiple different properties on the sut(s). (I know you can compare whole structs, but sometimes it's not just ==, sometimes your only checking certain fields, etc.