Will Policiano
Policiano

Policiano

Make your tests cleaner using the Fixture Pattern

Make your tests cleaner using the Fixture Pattern

Will Policiano's photo
Will Policiano
·Feb 2, 2022·

4 min read

Play this article

Every unit test has an arrangement step where we set up the test preconditions. In this step, we often need to provide some input objects to the subject under test.

Providing these objects can become pretty verbose and painful to write. Let's see some example:

func testValidateOrder_deliversErrorOnEmptyOrder() {
    let emptyItems: [Item] = []
    let anySeller = Seller(uuid: "any UUID", name: "any name", websiteUrl: URL(string: "http://any-url.com")!)
    let order = Order(uuid: "any UUID", date: Date(), items: emptyItems, totalPrice: 0.0, discount: 0.0, seller: anySeller)

    let actualResult = sut.validate(order: order)

    XCTAssertEqual(actualResult, OrderValidationError.emptyOrder)
}

This is a simple method that validates an order. In this case, orders without items are not allowed and are expected to return an error. Look how verbose and hard to understand this method became. The cause of it is the complexity of the arrangement. To perform the validate(order:) we need to create an Order, which is not a trivial object. Also, the Order object is not an test double but an actual system object type.

Therefore, to improve it, we need to make our test cleaner and focused on what matters by abstracting the object creation and simplifying the arrangement step. We can do it with Fixtures.

Fixtures

Before all, compare the previous example with this new one:

func testValidateOrder_deliversErrorOnEmptyOrder() {
    let order = Order.fixture(items: [])

    let actualResult = sut.validate(order: order)

    XCTAssertEqual(actualResult, OrderValidationError.emptyOrder)
}

Much cleaner, uh? I could abstract the Order creation and focus on what really matters: Setting up an empty order.

This kind of abstraction is called fixture and we can find a great formal definition for test fixtures on the JUnit 4 documentation:

A fixed state of a set of objects used as a baseline for running tests. The purpose of a test fixture is to ensure that there is a well-known and fixed environment in which tests are run so that results are repeatable.

In other words, a fixture is a good way to ensure good code readability on abstracting objects creation. Let's see how to implement it in Swift.

Implementing a Fixture

Because creating fixtures is related to testing purposes, it's recommended to implement it in the Test target. I will use this file naming:

Order+Fixtures.swift

Now we can add extensions to our objects and create the fixture this way:

extension Order {
    static func fixture(
        uuid: String = "", 
        date: Date = Date(timeIntervalSince1970: 0), 
        items: [Item] = [], 
        totalPrice: Decimal = 0.0, 
        discount: Decimal = 0.0, 
        seller: Seller = .fixture()
    ) -> Order {
        return Order(
            uuid: uuid,
            date: date, 
            items: items, 
            totalPrice: totalPrice, 
            discount: discount, 
            seller: seller
        )
    }
}

extension Seller {
    static func fixture(
        uuid: String = "",
        name: String = "",
        websiteUrl: URL = .fixture()
    ) -> Seller {
        return Seller(
            uuid: uuid,
            name: name,
            websiteUrl: websiteUrl
        )
    }
}

extension URL {
    static func fixture(string: String = "") -> URL {
        return URL(fileURLWithPath: string)
    }
}

Simple as that, but we need to consider some things in my implementation:

  1. Note that I created a new fixture recursively for each dependency. It's up to you. Create fixtures as you go and feel that it's needed
  2. I created every fixture in a single file by convenience. You can separate them into different files if you want. No problem.
  3. Double attention to the default values. Aways prefer using empties, zeroes and non-failable values on fixtures default values. Otherwise, we can be lead to flakiness, especially when working with Date.
  4. Why not a convenience init? Using a convenience init wouldn't work because you will be creating a new init with the same argument labels that you defined on the production code, and the compiler will complain about it. On the other hand, using the fixture term tells the reader the code is getting a test value more explicitly.

Conclusion

This was a short tip on how you can make your tests cleaner by arranging the test cases with Fixtures. The way I implemented is a humble suggestion to inspire you. Put it in practice. I strongly believe that it will bring you awesome results. There are a bunch of more techniques to write better tests I will explore with you during this year, so stay tuned. See ya!


References:

 
Share this