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