If you’ve contributed to Tuist in the past, you might have noticed that we subclassed XCTestCase
and used the setUp
and tearDown
functions to mutate global state. While convenient, this prevented parallelization because it would cause race conditions.
For a long time, we’ve been thinking about moving away from there, but doing so without compromising the ergonomics of our code was something that neither the language nor the testing framework provided answer for until two things happened: they introduced @TaskLocals in Swift, and custom execution traits in Swift Testing. And because a piece of code is worth a thousand words. This is what we achieve with it:
@Test(
.withTestingSimulator("iPhone 16"),
.withMockedEnvironment,
)
func acceptance_test() {
let simulator = try #require(Simulator.testing)
// Any deep piece of code can use Environment.current
// to get the environment
}
In the example above, the environment and the simulator are scoped to that test, so the state doesn’t leak into other tests. Isn’t it amazing?
The problem is that we still have many XCTests in our codebase, so we’ll need your help:
- If you add a new test, implement it with Swift Testing. If adding it to an existing XCTestCase, consider migrating the whole test.
- Use the existing traits and utilities for Swift Testing (under
TuistTest
andTuistAcceptanceTest
) and create new ones if needed. - Don’t abuse TaskLocals. They make sense for variables that can be global throughout the lifecycle of the CLI.
With your help, we’ll be able to parallelize, reducing the CI turnaround time quite significantly.