During my last project it became apparent that we had no consistency in how we created the data used in our unit tests; listed below are some of the various different solutions that had been implemented
- Private helper methods that created entities, often these methods had been cut and paste into other unit test classes
- Creating and setting properties on the entities in the test method
- Several helper classes that were used by some of the unit tests
All of the above highlighted firstly that we were not treating our unit test classes as first class citizens (e.g. the tests did not always following the coding standards used, were poorly commented and there was no code reuse between any of the tests). To fix this going forwards we started to treat unit tests as first class citizens and implementing them as we would implement a feature in the main code base.
To address the problem of how to manage the data used in the unit tests we decided to use the object builder pattern; the object builder pattern enabled us to have:
- A single and consistent method of creating entities
- Easy to extend to enable different property values to be set
- Fluent API
- Ability to chain object builders to compose objects as required
Below is the OrderProcess class that I am going to use to demonstrate how to write unit tests that utilize the object builder pattern for creating test data.
/// <summary>
/// Order process class
/// </summary>
public class OrderProcess
{
/// <summary>
/// Checks to verify that the customer has a enough
/// credit to place the order
/// </summary>
/// <param name="order">Order to verify</param>
/// <exception cref="ValidationException">Exception thrown if customer has insufficient credit</exception>
public void VerifyCustomerCredit(Order order)
{
// Get the total cost of the order
int orderCost = order.Products.Sum(product => product.Price);
// Checks the customer has sufficient credit
if (orderCost > order.Customer.Credit)
{
throw new ValidationException("Order cannot be processed, customer does not have enough credit");
}
}
/// <summary>
/// Checks to verify that order does not contain more than the
/// maximum number of items that the customer is allowed to order
/// </summary>
/// <param name="order">Order to verify</param>
/// <exception cref="ValidationException">Exception thrown if customer has created an order with too many items</exception>
public void VerifyNumberOfItems(Order order)
{
if (order.Products.Count > order.Customer.MaximumItemsPerOrder)
{
throw new ValidationException("Order cannot be processed, too many items in order");
}
}
/// <summary>
/// Applies a discount to the order
/// </summary>
/// <param name="order">Order to apply the discount to</param>
public void ApplyDiscount(Order order)
{
// Only apply the discount if the customer is a premium customer
if (CustomerStatus.Premium.Equals(order.Customer.Status))
{
order.Discount = 50;
}
}
}
To help test the OrderProcess class I’ve implemented an object builder per entity (Order, Product and Customer). The object builders are shown below.
/// <summary>
/// Object builder for creating test customer objects
/// </summary>
public class CustomerBuilder
{
/// <summary>
/// Customer credit limit
/// </summary>
private int credit = 5000;
/// <summary>
/// Customer Id
/// </summary>
private int id = 1;
/// <summary>
/// Maximum number of items the customer can place per order
/// </summary>
private int maximumItemsPerOrder = 100;
/// <summary>
/// Customer status
/// </summary>
private CustomerStatus status = CustomerStatus.Basic;
/// <summary>
/// Specifies the customer's credit limit
/// </summary>
/// <param name="credit">Credit limit</param>
/// <returns>Builder instance</returns>
public CustomerBuilder WithCredit(int credit)
{
this.credit = credit;
return this;
}
/// <summary>
/// Specifies the customer's maximum items per order
/// </summary>
/// <param name="maximumItemsPerOrder">Maximum number of items per order</param>
/// <returns>Builder instance</returns>
public CustomerBuilder WithMaximumItemsPerOrder(int maximumItemsPerOrder)
{
this.maximumItemsPerOrder = maximumItemsPerOrder;
return this;
}
/// <summary>
/// Specifies the customer's status
/// </summary>
/// <param name="status">Customer status</param>
/// <returns>Builder instance</returns>
public CustomerBuilder WithStatus(CustomerStatus status)
{
this.status = status;
return this;
}
/// <summary>
/// Builds a Customer entity
/// </summary>
/// <returns>Customer entity</returns>
public Customer Build()
{
return new Customer
{
Id = this.id,
Credit = this.credit,
MaximumItemsPerOrder = this.maximumItemsPerOrder,
Status = this.status
};
}
}
As you can see the CustomerBuilder class has a default value definied for each of the properties on the Customer entity and if you simply called the Build method it would return a Customer entity with every property set to a sensible default value. If you need to be able to set a particular property on the Customer object you simple add a WithXXX method for example to set the customer’s status you would call the WithStatus method specifying the required status. You will notice the WithXXX methods all return an instance of the object builder therefore allowing you to chain method calls for example to set both the customer’s credit and status you would call the builder as follows.
Customer customer = new CustomerBuilder()
.WithCredit(60)
.WithStatus(CustomerStatus.Premium)
.Build();
Below are the ProductBuilder and OrderBuilder classes
/// <summary>
/// Object builder for creating test product objects
/// </summary>
public class ProductBuilder
{
/// <summary>
/// Product's id
/// </summary>
private int id = 1;
/// <summary>
/// Product's name
/// </summary>
private string name = "My Product";
/// <summary>
/// Product's price
/// </summary>
private int price = 10;
/// <summary>
/// Specifies the product's price
/// </summary>
/// <param name="price">Product's price</param>
/// <returns>Builder instance</returns>
public ProductBuilder WithPrice(int price)
{
this.price = price;
return this;
}
/// <summary>
/// Builds a Product entity
/// </summary>
/// <returns>Product entity</returns>
public Product Build()
{
return new Product
{
Id = this.id,
Name = this.name,
Price = this.price
};
}
}
/// <summary>
/// Object builder for creating test order objects
/// </summary>
public class OrderBuilder
{
/// <summary>
/// List of products that make up the order
/// </summary>
private readonly List<Product> products = new List<Product>();
/// <summary>
/// Customer who placed the order
/// </summary>
private Customer customer = new CustomerBuilder().Build();
/// <summary>
/// Order discount
/// </summary>
private int discount;
/// <summary>
/// Order id
/// </summary>
private int id = 1;
/// <summary>
/// Specifies the customer who placed the order
/// </summary>
/// <param name="customer">Customer who placed the order</param>
/// <returns>Builder instance</returns>
public OrderBuilder WithCustomer(Customer customer)
{
this.customer = customer;
return this;
}
/// <summary>
/// Specifies a product to add to the order
/// </summary>
/// <param name="product">Product to add to the order</param>
/// <returns>Builder instance</returns>
public OrderBuilder WithProduct(Product product)
{
this.products.Add(product);
return this;
}
/// <summary>
/// Builds an Order entity
/// </summary>
/// <returns>Order entity</returns>
public Order Build()
{
return new Order
{
Id = this.id,
Customer = this.customer,
Products = this.products,
Discount = this.discount
};
}
}
The OrderBuilder class is more complex than the previous builders and makes use of the other builders, for example the Order entity has a Customer property instead of creating a new instance of a Customer entity the OrderBuilder makes use of the CustomerBuilder to ensure that by default it has a fully and correctly constructed Customer entity. The Order entity also contains a list of products to help populate the list the OrderBuilder exposes a WithProduct method which adds the specified product to the list. Shown below is an example of how all three object builders could be chained together.
Order order =
new OrderBuilder()
.WithProduct(new ProductBuilder().WithPrice(100).Build())
.WithProduct(new ProductBuilder().WithPrice(200).Build())
.WithCustomer(new CustomerBuilder().WithCredit(50).Build())
.Build();
Finally shown below is the OrderProcessTest class which is responsible for testing the OrderProcess class, to achieve this it makes use of the object builders shown above.
/// <summary>
/// OrderProcess tests
/// </summary>
[TestClass]
public class OrderProcessTests
{
/// <summary>
/// Tests that a ValidationException is thrown when the customer does not have
/// enough credit to pay for the order
/// </summary>
[TestMethod]
[ExpectedException(typeof(ValidationException))]
public void VerifyCustomerCredit_When_Customer_Credit_Less_Than_Order_Cost()
{
// Arrange
Order order =
new OrderBuilder()
.WithProduct(new ProductBuilder().WithPrice(100).Build())
.WithProduct(new ProductBuilder().WithPrice(200).Build())
.WithCustomer(new CustomerBuilder().WithCredit(50).Build())
.Build();
// Act
OrderProcess process = new OrderProcess();
process.VerifyCustomerCredit(order);
}
/// <summary>
/// Tests that no ValidationException is thrown when the customer does have
/// enough credit to pay for the order
/// </summary>
[TestMethod]
public void VerifyCustomerCredit_When_Customer_Credit_Greater_Than_Order_Cost()
{
// Arrange
Order order =
new OrderBuilder()
.WithProduct(new ProductBuilder().WithPrice(100).Build())
.WithProduct(new ProductBuilder().WithPrice(200).Build())
.WithCustomer(new CustomerBuilder().WithCredit(5000).Build())
.Build();
// Act
OrderProcess process = new OrderProcess();
process.VerifyCustomerCredit(order);
}
/// <summary>
/// Tests that a ValidationException is thrown when the customer has created an
/// order with too many items
/// </summary>
[TestMethod]
[ExpectedException(typeof(ValidationException))]
public void VerifyNumberOfItems_When_Maximum_Items_Exceeded()
{
// Arrange
Order order =
new OrderBuilder().WithProduct(new ProductBuilder().Build())
.WithProduct(new ProductBuilder().Build())
.WithProduct(new ProductBuilder().Build())
.WithCustomer(new CustomerBuilder().WithMaximumItemsPerOrder(2).Build())
.Build();
// Act
OrderProcess process = new OrderProcess();
process.VerifyNumberOfItems(order);
}
/// <summary>
/// Tests that the discount is applied for a premium customer
/// </summary>
[TestMethod]
public void ApplyDiscount_When_Customer_Status_Premium()
{
// Arrange
Order order =
new OrderBuilder()
.WithCustomer(new CustomerBuilder().WithStatus(CustomerStatus.Premium).Build())
.Build();
// Act
OrderProcess process = new OrderProcess();
process.ApplyDiscount(order);
// Assert
Assert.AreEqual(50, order.Discount);
}
/// <summary>
/// Test that the discount is not applied for non premium customers
/// </summary>
[TestMethod]
public void ApplyDiscount_When_Customer_Status_Basic()
{
// Arrange
Order order =
new OrderBuilder()
.WithCustomer(new CustomerBuilder().WithStatus(CustomerStatus.Basic).Build())
.Build();
// Act
OrderProcess process = new OrderProcess();
process.ApplyDiscount(order);
// Assert
Assert.AreEqual(0, order.Discount);
}
}