This article is part of a series on leveling up your testing skills
- Creating Test Objects via Design Patterns
- Supercharging Your Test Data With AutoFixture
- Creating Domain-Driven Test Data With Bogus
- Boosting the Builder Pattern Using Bogus
When writing tests, you may find it tedious to create test objects. There are design patterns that we can use to make it easier to create test objects. I want to talk about what I call “The Testing Design Pattern Pyramid” - the Factory Pattern, Object Mother Pattern, and Builder Pattern. I call it a pyramid because Factory allows for the least amount of customization where as the Builder allows the most customization, and the Object Mother is in the middle.
Factory Pattern
The Factory Pattern - also known as the Factory Method pattern and the Abstract Factory pattern - is typically used when you want to create instances of objects with specific configurations or default values, especially when you have a hierarchy of objects. The key with this pattern is that there is a singular Create method. Its job is to create an object.
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductFactory
{
public Product CreateProduct(string name, decimal price)
{
return new Product { Name = name, Price = price };
}
}
class Program
{
static void Main()
{
ProductFactory factory = new ProductFactory();
Product product1 = factory.CreateProduct("Product A", 10.99m);
Product product2 = factory.CreateProduct("Product B", 24.99m);
Console.WriteLine($"Product 1: {product1.Name}, Price: {product1.Price:C}");
Console.WriteLine($"Product 2: {product2.Name}, Price: {product2.Price:C}");
}
}
You can use the Factory Pattern to create test objects with consistent default values or configurations. It’s useful when you have a complex object creation process, and you want to encapsulate that complexity.
In unit testing, you might use a factory to create mock objects, stubs, or instances of your classes for testing. For instance, you could create a ProductFactory that generates product objects with different categories and prices for testing.
Object Mother Pattern
The Object Mother Pattern is used when you want to create and configure a group of objects with specific attributes or states to be used consistently across multiple test cases. It’s suitable for scenarios where you need predefined test data. The Object Mother pattern does allow for some customizations, through multiple Create options.
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductObjectMother
{
public Product CreateExpensiveProduct()
{
return new Product { Name = "Expensive Product", Price = 99.99m };
}
public Product CreateAffordableProduct()
{
return new Product { Name = "Affordable Product", Price = 19.99m };
}
}
class Program
{
static void Main()
{
ProductObjectMother objectMother = new ProductObjectMother();
Product expensiveProduct = objectMother.CreateExpensiveProduct();
Product affordableProduct = objectMother.CreateAffordableProduct();
Console.WriteLine($"Expensive Product: {expensiveProduct.Name}, Price: {expensiveProduct.Price:C}");
Console.WriteLine($"Affordable Product: {affordableProduct.Name}, Price: {affordableProduct.Price:C}");
}
}
Object Mothers are useful for setting up a consistent test environment by providing pre-configured objects or object graphs with various states. This pattern is often used in integration or system testing.
In integration testing, you might use an OrderObjectMother to create order-related objects with specific data (e.g., customers, products, and orders) to simulate various scenarios during testing.
Builder Pattern
The Builder Pattern is used when you need to construct complex objects step by step and provide flexibility in configuring their attributes. It’s particularly useful when there are many optional parameters to set.
This is commonly seen with classes with a large amount of customizable parameters. The Builder class has a private variable to hold the base object. Use With...
methods to allow for a specific parameter to be set. The Builder has a Build()
method to return the object with all of its customizations.
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductBuilder
{
private Product product = new Product();
public ProductBuilder WithName(string name)
{
product.Name = name;
return this;
}
public ProductBuilder WithPrice(decimal price)
{
product.Price = price;
return this;
}
public Product Build()
{
return product;
}
}
class Program
{
static void Main()
{
Product expensiveProduct = new ProductBuilder()
.WithName("Expensive Product")
.WithPrice(99.99m)
.Build();
Product affordableProduct = new ProductBuilder()
.WithName("Affordable Product")
.WithPrice(19.99m)
.Build();
Console.WriteLine($"Expensive Product: {expensiveProduct.Name}, Price: {expensiveProduct.Price:C}");
Console.WriteLine($"Affordable Product: {affordableProduct.Name}, Price: {affordableProduct.Price:C}");
}
}
The Builder Pattern is valuable for creating test objects with customizable attributes. It’s especially useful when you need to set a specific combination of properties for your test objects.
In unit testing, you could use a builder to construct instances of a class with various configurations. For instance, you might have a ProductBuilder that allows you to set different product attributes like name, price, and category during object creation.
If you want to see another example of the Builder pattern, check out Ardalis’ post on Improve Tests with the Build Pattern for Test Data.
Conclusion
So when you’re working in tests, which of these patterns should you be using?
-
Use the Factory Pattern when you want to encapsulate object creation and provide consistent object creation logic for test objects, especially in unit testing.
-
Use the Object Mother Pattern when you need to set up a consistent environment with pre-configured test objects for integration or system testing.
-
Use the Builder Pattern when you want to create complex objects step by step with the ability to customize their attributes in a flexible way, primarily in unit testing.
If you have a developer team that needs more guidance on TDD or design patterns, bring us in to help your developers work with better software development practices. Contact us today!