Fixtures
Tests should be easy to read, write, and maintain. To achieve that, test methods must be as concise as possible and that’s where fixtures come in handy.
A fixture consist of a type with helper methods to hide the complexity of creating certain types used in tests, or as Martin Fowler calls them, ObjectMothers:
An object mother is a kind of class used in testing to help create example objects that you use for testing. Object Mother is just a catchy name for such a factory. The name was coined on a Thoughtworks project at the turn of the century and it’s catchy enough to have stuck.
In the example below, for instance, RouteFixture
is a fixture factory which builds a Route
object used in a test. The Route
has an initializer with multiple parameters, and repeating this initialization in every test methods would just increase the complexity of the tests.
enum RouteFixture {
static func makeRoute() -> Route {
Route(
origin: .init(lat: 52.3667544, long: 13.4607836),
destination: .init(lat: 52.516366, long: 13.377178),
vehicle: .car,
type: .shortestRoute,
time: Date()
)
}
static func makeComplexRoute() -> Route { /* ... */ }
}
When the fixture is used, the test function becomes more readable and descriptive. The Route
complexity is taken out of the equation, leaving only the code that is relevant to the test.
func testViewModelWithValidRoute() {
let route = RouteFixture.makeRoute()
let viewModel = RouteViewModel(route: route)
XCTAssertEqual(
viewModel.route,
route,
"It should have the correct route"
)
}
In most cases, a fixture is used in several other test cases and therefore it’s a good practice to have them in their own files.
Creating dedicated factory types
This is the pattern used in the example above. It consists of factories for each type they construct.
enum FooFixture {
static func makeFoo() -> Foo
static func makeAnotherFoo() -> Foo
}
enum BarFixture {
static func makeBar() -> Bar
static func makeAnotherBar() -> Bar
}
Usage:
func testSomething() {
let foo = FooFixture.makeFoo()
let bar = BarFixture.makeAnotherBar()
// ...
}
Creating namespaces
An elegant solution for building fixtures is using nested types, building a namespace for the factories. This improves readability and discoverability. In the example below, all the methods which generate Foo
are under Fixture.Foo
.
enum Fixture {}
extension Fixture {
enum Foo {
static func makeFoo() -> Foo
}
enum Bar {
static func makeBar() -> Bar
}
}
Usage:
func testSomething() {
let foo = Fixture.Foo.makeFoo()
let bar = Fixture.Bar.makeBar()
// ...
}
Extending types
Another approach is to simply extend types, adding new factory methods used to create new instances. Although simple, this solution mixes production and testing code. It also makes discoverability a little harder.
extension Foo {
static func makeFixture() -> Foo
}
extension Bar {
static func makeFixture() -> Bar
}
Usage:
func testSomething() {
let foo = Foo.makeFixture()
let bar = Bar.makeFixture()
// ...
}
No matter the strategy used to create new instances, the idea behind fixtures is the same: keeping tests easier to read and write, hiding all the complexity of initializing complex types.
Bonus: As Fakes, Fixtures are interesting when working with SwiftUI previews. Keeping the PreviewProvider
implementation simple and easy to read and maintain.
References
- ObjectMother by Martin Fowler.
Where to go from here: