A subtlety of TDD for Swift and XCTest
The principles of test-driven development (TDD) apply equally to all programming languages. At the broadest level, TDD in Java is the same as TDD in Swift, for example.
The different unit testing frameworks have their own quirks as to how these principles are applied, however.
And so, the TDD experience with JUnit or any other unit testing framework for the Java Virtual Machine is different from the TDD experience with XCTest for Swift in some unexpectedly subtle ways.
This article is about one of those unexpected subtleties that I just came across earlier today.
I was supposed to be programming a card game, and I got hung up on unit testing the suit enumeration when everyone else had forged ahead with figuring out how to implement the rules of the particular card game we had been assigned.
This is what I had:
enum Suit: Character, CaseIterable {
// TODO: Write tests for this
case spades = "\u{2650}"
case diamonds = "\u{2656}"
case clubs = "\u{2653}"
case hearts = "\u{2655}"
}
Simple enough and perhaps not even worth testing. But since I’m still getting the hang of XCTest, I need to start out with simple things that are easily tested.
The TDD cycle is fail, pass, refactor. Lather, rinse, repeat. The Suit
enumeration as it stands now should fail whatever test I write for the card suit symbols. This is the test I wrote:
func testSuitSymbol() {
for suit in Suit.allCases {
let expected: Character = switch (suit) {
case .spades:
"\u{2660}"
case .diamonds:
"\u{2666}"
case .clubs:
"\u{2663}"
case .hearts:
"\u{2665}"
}
let actual: Character = suit.rawValue
XCTAssertEqual(expected, actual)
}
}
We don’t need Breaks here, that’s a feature of Swift, not XCTest. The indentation for the Switch-Case feels off to me, but it’s apparently the default indentation in Xcode.
From my experience with Java and JUnit, my expectation here was that the assertion would fail for spades, and that would be the end of the suit symbol test. But that’s not what happened.
With a unit testing framework for Java, be it JUnit, TestNG, even my own unit testing framework I’ve created from scratch, when an assertion fails, the test runner records the test as failing and makes no effort whatsoever to continue running that same test.
Not so with XCTest. It records the test as failing but then hands control back to the test that failed.
There are certainly benefits to this. Whether deliberately or not, XCTest is less opinionated than the Java unit testing frameworks about trying to enforce having only one assertion per unit test.
The way XCTest handles multiple assertions in a single unit test gives me greater confidence in changing the class under test to pass the test more quickly than I would with JUnit with this particular test.
And then I decided to add Variation Selector 16 to each of the suit symbols.
It looks funny to a Java developer to put two Unicode characters in a single Swift Character
instance, but that’s a whole other issue outside the scope of this article.
If we wanted this behavior of not having an assertion failure stop a test in a Java unit testing framework, we could certainly add it. I surmise we would have to radically rethink both the test runner and the assertions library.
But it would also require pondering whether we regard the prescription that each unit test should only have one assertion as a foundational principle of TDD or just a suggestion.