Validating your code with custom assertions
The Problem
In the first part of this series I defined what I think of as a “good” unit test. This is the definition that I ended up at:
If we can agree that a unit test (or, really, any test) is composed of some setup, the action that we’re testing, and then an assertion about the effect of that action, then I would say that, put simply, a “good” unit test is one which makes each of those three components clear.
You might also remember that we finished that post with tests that looked like this:
var sut, other: User!
func test\_equals\_allPropertiesMatch\_isTrue() {
(sut, other) = (.create(), .create())
XCTAssertEqual(sut, other)
XCTAssertEqual(other, sut)
}
func test\_equals\_nameDiffers\_isFalse() {
(sut, other) = (.create(name: "Jo"), .create())
XCTAssertNotEqual(sut, other)
XCTAssertNotEqual(other, sut)
}
We compressed the setup so that the test body contained only the information that was important to the scenario of the test. For example, when all that mattered was that the names of the two user objects differed, we included only that information in the setup.
This goes a great way to improving the overall readability of the test. But there’s even more we can do.
It’s possible that you looked at the above tests and thought “the setup’s okay, but why the heck have you got two asserts there?” Great question! I’m so glad you’re paying attention.
Remember that the above tests are covering the == function on a User type. Without dropping a whole load of maths on you, equality is an example of an “equivalence relation” and one of the important things about being an equivalence relation is the concept of symmetry. Put simply, if I have two instances a and b of some type, it should never be the case that a == b is true but b == a is false. If this is possible, then our definition of equality is flawed. So it makes sense to validate that our custom definition of == is symmetric.
But simply looking at our two asserts there, it’s absolutely not clear that this is the intention. These tests are only really asserting one thing. It’s possible that, with some time and the inclination, other engineers could work out why we added the second assert, but we can definitely do better.
But…how?
The Solution
Let’s start by doing the simplest thing we can possibly think of. We have two lines of code, and we’d like to have only one line of code. The solution? A function!
func assertSymmetricallyEqual(\_ sut: User, \_ other: User) {
XCTAssertEqual(sut, other)
XCTAssertEqual(other, sut)
}
(Note: we’re focusing in on testing for equality here, but you can do the exact same thing with non-equality).
Now our test becomes:
var sut, other: User!
func test\_equals\_allPropertiesMatch\_isTrue() {
(sut, other) = (.create(), .create())
assertSymmetricallyEqual(sut, other)
}
If we run this test and it passes, everything’s good. But when the assertions fail, Xcode shows the failures in the wrong place.
Clearly this isn’t acceptable. If we’re trying to diagnose a failing test quickly, we want to be able to see exactly what’s failed and where. More to the point, if two or three tests all fail using this same assert function then we’re going to have a hard time separating the failures out.
Fortunately, the XCTAssertEqual documentation provides a solution. There we can see the Swift declaration of the function, which is as follows:
func XCTAssertEqual\<T\>(
\_ expression1: @autoclosure () throws -\> T,
\_ expression2: @autoclosure () throws -\> T,
\_ message: @autoclosure () -\> String = default,
**file: StaticString = #file,
line: UInt = #line** ) where T : Equatable
I’ve made the two most important lines bold.
Since XCTAssertEqual is a function in Swift (rather than a macro, as it was in Objective-C), there’s a slight trick to get Xcode to render failures in the correct place. When we call XCTAssertEqual the Swift compiler adds the file it was called from (#file), and the line it was called on (#line), to the call as default arguments.
Since our calls to XCTAssertEqual don’t happen on the line we want to see the failure on, we need to put in a bit of extra effort to get everything working. Essentially, we need to tell XCTAssertEqual the correct file and line to show failures on. This should be the #file and #line that our custom assert method is called on. So, following the example from the Apple documentation, we end up with this:
func assertSymmetricallyEqual(
\_ sut: User, \_ other: User,
**file: StaticString = #file,
line: UInt = #line**
) {
XCTAssertEqual(sut, other, **file: file, line: line** )
XCTAssertEqual(other, sut, **file: file, line: line** )
}
The changes to the method have been marked in bold.
Running the tests again, we get the following result:
Alright! This is exactly what we were looking for. Now the failures show up exactly where we need to see them. 🙌
The Refactor
What we’ve done above is fine, but as soon as we want to assert that more than one type satisfies symmetric equality we’re going to find ourselves with a one way ticket to Duplication Town. 🏘🏘🏘🏘
The solution? For that we can refer back to the documentation from earlier. Swift’s XCTAssertEqual method is generic over some T: Equatable — so let’s just do the exact same thing to our assertSymmetricallyEqual.
func assertSymmetricallyEqual **\<T: Equatable\>** (
\_ sut: **T** , \_ other: **T** ,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertEqual(sut, other, file: file, line: line)
XCTAssertEqual(other, sut, file: file, line: line)
}
Changes marked in bold again. And look at that! There’s hardly any of them!
Now we have a nice, reusable custom assert that confirms that two instances of an Equatable type are equal. What a time to be alive!
The Bonus
At Clue, we’ve actually taken this pattern a small step further. We have an internal framework which we use for test helpers which are needed across different modules. This framework contains the following struct:
public struct Assert\<T\> {
private let subject: T?
init(\_ subject: T?) {
self.subject = subject
}
}
“Alright, great,” I hear you cry, “but this does absolutely nothing.” And you’re right! Because the real magic happens in the extensions to this type:
extension Assert where T: Equatable {
public func symmetricallyEqual(
to other: T,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertEqual(subject, other, file: file, line: line)
XCTAssertEqual(other, subject, file: file, line: line)
}
}
And when we need to call this method, it looks like this:
Assert(sut).symmetricallyEqual(to: other)
Beautiful!
And, as a special treat, here’s an example of how we can extend this pattern to other types of assert. Here’s my own personal favourite custom assert function, which lets us assert that a specific error was thrown:
extension Assert where T: Error & Equatable {
public func isThrownIn(
\_ expression: @autoclosure () throws -\> (),
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertThrowsError(
try expression(), file: file, line: line
) {
XCTAssertEqual(
subject, $0 as? T,
file: file, line: line
)
}
}
}
(I had to take the indentation to slightly absurd levels to get this to fit in a Medium code block. Sorry!)
We can then use this as follows:
Assert(Errors.someError).isThrownIn(try someMethodThatThrows())
Oooh! ✨
Top comments (0)