Test Data Builders
The Test Data Builder pattern makes it easier to create input data or objects for unit tests
Why
Using Test Data Builders in your tests will:
- Facilitate writing tests by allowing easy creation of inputs or expected data.
- Facilitate test maintainability by decoupling the creation of objects in tests, and isolating it in a single location (Single Responsibility Principle)
- Disseminate functional understanding in the team through the use of business terms
- Facilitate the readability of the tests by explaining only the data implied in the use case
Problems
- How to decouple object creation from implementation in your tests?
- How to improve code readability in your tests?
- How to speed up writing your test setup?
How to
The 4 rules of test data builders
- Have an instance variable for each constructor parameter
- Initialize its instance variables to commonly used or safe values
- Has a
build
method that creates a new object using the values in its instance variables - Has
chainable
public methods for overriding the values in its instance variables
Examples
Considering the following command to create a property :
public class CreatePropertyCommand
{
public string Label { get; set; }
public bool? IsRanged { get; set; }
public bool? IsNumeric { get; set; }
public bool? IsMultiple { get; set; }
public IEnumerable<string> Choices { get; set; }
}
Without usage of test data builder
In each test on property creation, you will include quite the same command creation:
var command = new CreatePropertyCommand ("label", true, false, true, new string[] {1, 2});
- Every time your object creation will change, you have to fix all tests the class is implied
- What does the
false
value means?!? - What if the constructor arguments were not primitive types but required the creation of other objects?
With test data builders
public class CreatePropertyCommandBuilder
{
private string _label;
private bool _isMultiple = true;
private bool _isRanged = false;
private bool _isNumeric = true;
private string[] _choices = new string[0];
public static CreatePropertyCommandBuilder APropertyCreation
=> new CreatePropertyCommandBuilder();
public CreatePropertyCommandBuilder WithLabel(string label)
{
_label = label;
return this;
}
public CreatePropertyCommandBuilder WithChoices(string[] choices)
{
_choices = choices;
return this;
}
public CreatePropertyCommand Build()
{
return new CreatePropertyCommand(_label, _isMultiple, _isRanged, _isNumeric, _choices);
}
}
Using your builder in the tests
- Decouple object construction from its implementation, reducing refactoring effort on changes
- Explains data implied in the test case
//Arrange
var command = CreatePropertyCommandBuilder
.APropertyCreation
.WithLabel("Ma nouvelle propriété")
.Build();
// ...
// Assert
property.Label
.Should()
.Be("Ma nouvelle propriété");
Constraint
Replace redundant object creation by test data builders