Tidy up repetitive XCTests by removing boilerplate so you can focus on each test’s inputs and outputs.
Here is an existing XCTest that confirms that a Version
type’s ExpressibleByStringLiteral
is working by comparing the results to known good values:
// Before
func testExpressibleByStringLiteral() {
XCTAssertEqual("1", Version(major: 1)),
XCTAssertEqual("1.0", Version(major: 1)),
XCTAssertEqual("1.0.0", Version(major: 1)),
XCTAssertEqual("1.2", Version(major: 1, minor: 2)),
XCTAssertEqual("4.5.6", Version(major: 4, minor: 5, bugfix: 6)),
XCTAssertEqual("10.1.88", Version(major: 10, minor: 1, bugfix: 88)),
}
Using TestCleaner, we can clean this up a little without sacrificing Xcode’s ability to highlight lines containing failing tests. We also recommend using a local typealias
to help reduce line noise:
// After
func testExpressibleByStringLiteral() {
typealias V = Version
assertEqual(testCases: [
Pair("1", V(major: 1)),
Pair("1.0", V(major: 1)),
Pair("1.0.0", V(major: 1)),
Pair("1.2", V(major: 1, minor: 2)),
Pair("4.5.6", V(major: 4, minor: 5, bugfix: 6)),
Pair("10.1.88", V(major: 10, minor: 1, bugfix: 88)),
])
}
Now, the assert operation (“equal”) needs to be written in just one place, making the block of tests less error-prone and the intent clearer. It also reduces line length, although the typealias
is helping there.
Borrowing syntax from Quick, you can focus any tests by adding an f
to the beginning, and only those tests will execute on the next run, allowing you to debug individual cases without having to haphazardly comment and uncomment lines:
func testExpressibleByStringLiteral() {
typealias V = Version
assertEqual(testCases: [
Pair("1", V(major: 1)),
fPair("1.0", V(major: 1)), // Only this test will run.
Pair("1.0.0", V(major: 1)),
]
}
You can also skip test cases by prepending them with x
:
func testExpressibleByStringLiteral() {
typealias V = Version
assertEqual(testCases: [
Pair("1", V(major: 1)),
xPair("1.0", V(major: 1)), // These last two tests will be ignored.
xPair("1.0.0", V(major: 1)),
]
}
These can be combined in a single test for added flexibility: use xPair
to skip some tests to start with, then focus in on just one to debug it with fPair
.
TestCleaner mirrors the full range of XCTAssert functions available:
- Boolean (true/false)
- LessThan
<
- GreaterThan
>
- LessThanOrEqual
<=
- GreaterThanOrEqual
>=
- Equal
==
- Equal (with floating-point accuracy)
==
- NotEqual
!=
- NotEqual (with floating-point accuracy)
!=
If you have a custom assertion function, you can use the assertCustom
function to use it with TestCleaner. Just make sure your custom assertion takes file: StaticString, line: UInt
parameters, and forward the ones that the tests
closure in assertCustom
passes to you.
assertCustom(
testCases: [
Pair(someLeftValue, someRightValue),
Pair(anotherLeftValue, anotherRightValue),
],
tests: { pair, file, line in
myCustomAssertion(
try pair.left, try pair.right,
message: pair.message,
file: file, line: line // <-- ⚠️ this is important!
)
try youCanAlsoThrowErrorsInHere() // They will also get attributed to the correct line.
}
)
This tool is ideal for tests of pure transformations, where a certain input will always produce the same output. Examples include: parsing, mapping, converting, and calculating. It is not intended to be used for integration-style tests, where each assertion is preceded by many lines of setup.
TestCleaner was inspired by a blog post which was in turn inspired by a conversation with Brian King.
The spray bottle image was made using Blender, and the project file is hosted at ZevEisenberg/TestCleanerIcon.