Builder Pattern for creating Test Data

Photo by 许 婷婷 on Unsplash

Builder Pattern for creating Test Data

·

4 min read

When writing automated tests, we use test data to test certain use cases.

Common way

One common way is creating test data via a constant.

let restaurantWithOnlyName = Restaurant( 
    name: "Delicioso food", 
    cuisines: nil, 
    rating: nil 
)

And when you need to test a different use case. You need to create a new one.

let restaurantWithOnlyNameAndRating = Restaurant(
     name: "Delicioso food",
     cuisines: nil,
     rating: 5.0
)

Soon you have many constants. Making it hard to maintain.
Imagine you need to:

  • Change a property name

  • Add a new property

  • Delete a property

You have to update not only one but all of them.

A better solution: The Builder pattern

There are better ways of creating test data. Among them is the Builder pattern. Creating objects step by step, using a builder class.

Step 1:
To start with, every property has a default value.

class RestaurantBuilder {
    private var name: String = "Delicioso food"
    private var cuisines: [String]? = ["Seafood"]
    private var rating: Double? = 5.0
}

Step 2:
Second, every property’s value is changeable via a with(:) method.

class RestaurantBuilder {
    private var name: String = "Delicioso food"
    private var cuisines: [String]? = ["Seafood"]
    private var rating: Double? = 5.0

    func with(name: String) -> Self {
        self.name = name
        return self
    }

     func with(cuisines: [String]?) -> Self {
        self.cuisines = cuisines
        return self
    }

     func with(rating: Double?) -> Self {
        self.rating = rating
        return self
    }
}

Step 3:
And finally a build(:) method to construct the object using those values.

class RestaurantBuilder {
    private var name: String = "Delicioso food"
    private var cuisines: [String]? = ["Seafood"]
    private var rating: Double? = 5.0

    func with(name: String) -> Self {
        self.name = name
        return self
    }

     func with(cuisines: [String]?) -> Self {
        self.cuisines = cuisines
        return self
    }

     func with(rating: Double?) -> Self {
        self.rating = rating
        return self
    }

    func build() -> Restaurant {
        return Restaurant(
           name: self.name,
           cuisines: self.cuisines,
           rating: self.rating
        )
    }
}

Step 4:
After creating the builder class, we can use it.

When we are testing a use case, where the exact values of the properties don't matter, we can use the default values.

let restaurant = RestaurantBuilder().build()

And when we are testing a use case, where the exact values of the properties matter, we can change the default values.

let restaurant = RestaurantBuilder()
    .with(name: "Awesome Delicioso food")
    .build()

Benefits

  • Creating test data is made simple: It's fast and easy.

  • Keeping things maintainable: If the model changes, we only have to update the builder class. All the configuration takes place there.

  • Highlighting only what matters: You only change the values of the properties that matter. Removing all distractions.

// Create Restaurant with 5.0 star rating
let restaurantWithFiveStarRating = Restaurant(
     name: "Delicioso food",
     cuisines: nil,
     rating: 5.0
)

let restaurant = RestaurantBuilder()
    .with(rating: 5.0)
    .build()

Complex data

Data can be(come) complex. Imagine we want to provide the location of a Restaurant.

struct Location {
    let address: String?
    let zipCode: Int?
    let city: String?
    let country: String?
}

struct Restaurant {
    let name: String
    let cuisines: [String]?
    let rating: Int
    let location: Location?
}

We have to update our RestaurantBuilder to conform to this new change. To make this happen, we need to create a new builder class, LocationBuilder for the model Location. Then we will use this for setting the default value for the location property.

class RestaurantBuilder {
    private var name: String = "Delicioso food"
    private var cuisines: [String]? = ["Seafood"]
    private var rating: Double? = 5.0
    private var location: Location? = LocationBuilder().build()

    func with(name: String) -> Self {
        self.name = name
        return self
    }

     func with(cuisines: [String]?) -> Self {
        self.cuisines = cuisines
        return self
    }

     func with(rating: Double?) -> Self {
        self.rating = rating
        return self
    }

   func with(location: Location?) -> Self {
        self.location = location
        return self
    }

    func build() -> Restaurant {
        return Restaurant(
           name: self.name,
           cuisines: self.cuisines,
           rating: self.rating,
           location: self.location
        )
    }
}

Food for thought

Did you find this article helpful? Go ahead and give that 🤍 like button a tap.
Your appreciation fuels my motivation to keep bringing you more content 💪.

If you do have other ways of creating test data, feel free to share them in the comment section. Let's all learn new ways together!

References

Steve Freeman, S., & Nat Pryce, N., 2009. Growing Object-Oriented Software, Guided by Tests