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


Where to go from here:

  1. Introduction
  2. The Different Types of Tests
  3. Test Doubles
    1. Dummies
    2. Mocks
    3. Stubs
    4. Partials
    5. Fakes
  4. Fixtures
  5. Frameworks and Tools