Create Objects With Test Data Builders (Unit Test Helper)

In the previous blog post, I wrote about using helper methods instead of setup methods. For a recap, here are those C# helper methods that created MyCar objects used in unit tests:

private static MyCar CreateMyCar()
{
  var engine = new Engine("V8");
  var stereo = new Stereo("Sony");
  return new MyCar(engine, stereo);
}

private static MyCar CreateMyCarWithBigEngine()
{
  var bigEngine = new Engine("V16");
  var stereo = new Stereo("Sony");
  return new MyCar(bigEngine, stereo);
}

Maybe with these two tests, it is handy to have these two helper methods. If we imagine we need to create a car with a small engine, with Kenwood stereos, without stereos, etc. it wouldn’t be good to write helper methods for each different object. That would lead to code duplication easily.

Test Data Builder

Test data builders are helper classes to create domain objects. The previous example with test data builder could look like the following:

var car = new MyCarBuilder().Build();
var carWithBigEngine = new MyCarBuilder()
  .WithEngine("V16").Build();

The code of the MyCarBuilder is here:

public class MyCarBuilder
{
  private string _engine = "V8";  // engine's default value
  
  public MyCar Build()
  {
    return new MyCar(new Engine(_engine), new Stereo("Sony"));
  }
  
  public MyCarBuilder WithEngine(string engine)
  {
    _engine = engine;
    return this;
  }
}

This version doesn’t yet make clear what makes test data builders handy. When we add other “WithXxx” methods, it starts to make more sense since we can chain those method calls:

var carWithBigEngineAndPioneerStereo = new MyCarBuilder()
  .WithEngine("V16").WithStereo("Pioneer").Build();

Code for this could be like this:

public class MyCarBuilder
{
  private string _engine = "V8";
  private string _stereo = "Sony";
  
  public MyCar Build()
  {
    return new MyCar(new Engine(_engine), new Stereo(_stereo));
  }
  
  public MyCarBuilder WithEngine(string engine)
  {
    _engine = engine;
    return this;
  }

  public MyCarBuilder WithStereo(string stereo)
  {
    _stereo = stereo;
    return this;
  }
}

Big Help With Lists

If we compare the traditional way of creating a MyCar object and builder pattern, there isn’t a big difference in characters. In this case, the traditional way is even shorter:

var builderCar = new MyCarBuilder()
  .WithEngine("V16").WithStereo("Pioneer").Build();

var traditionalCar =
  new MyCar(new Engine("V16"), new Stereo("Pioneer"));

Builder pattern starts to pay back when the class is more complicated. Or when there is a need for different objects in tests. Imagine having a list of objects (List<passenger>) as a parameter in a constructor:

public MyCar(Engine engine, Stereo stereo,
  List<Passenger&gt; passengers)
{
  _engine = engine;
  _stereo = stereo;
  _passengers = passengers;
}

… and initializing that list in each test. That would become a nightmare at the latest when some tests want a different number of passengers in MyCar. Test data builder helps a lot with the lists. In the below example there is WithPassenger method that can be used to add as many passengers as wanted:

public class MyCarBuilder
{
  ...
  private List<Passenger&gt; _passengers = new List<Passenger&gt;();
  
  public MyCar Build()
  {
    return new MyCar(new Engine(_engine), new Stereo(_stereo),
      _passengers);
  }
  
  ...

  public MyCarBuilder WithPassenger(string name)
  {
    var passenger = new Passenger(name);
    _passengers.Add(passenger);
    return this;
  }
}

For example, MyCar with two passengers could be created like this:

var carWithTwoPassengers = new MyCarBuilder()
  .WithPassenger("Matsui").WithPassenger("Ichiro").Build();

If we compare that with the traditional way we can see how easy above test data builder is to use:

var engine = new Engine("V8");
var stereo = new Stereo("Sony");
var passengers = new List<Passenger&gt;();
passengers.Add(new Passenger("Matsui"));
passengers.Add(new Passenger("Ichiro"));
var carWithTwoPassengers = new MyCar(engine, stereo, passengers);

Helping Also With Default Values

Default values also make builders effective when creating different objects: we don’t have to pass new Engine("V8") each time because _engine = "V8" is the default value in the builder class (line 3). And default value for stereo is “Sony” (line 4).

public class MyCarBuilder
{
  private string _engine = "V8";
  private string _stereo = "Sony";
  
  public MyCar Build()
  {
    return new MyCar(new Engine(_engine), new Stereo(_stereo));
  }

  ...
}

When we create MyCar with the MyCarBuilder we don’t have to give any parameters because default values are in the builder class:

var carWithDefaultValues = new MyCarBuilder().Build();

When we compare this with the traditional way where we have to give parameters to Engine and Stereo each time, we can see immediately how frustrating it is to pass those values every time:

var carWithDefaultValues =
  new MyCar(new Engine("V8"), new Stereo("Sony"));

Using Builder(s) Within Builder

When we start to create test data builders quite soon there comes a situation where we use test data builder that uses another test data builder. This could be the case with MyCar also when we create EngineBuilder and StereoBuilder classes:

public class MyCarBuilder
{ 
  public MyCar Build()
  {
    var engine = new EngineBuilder().Build();
    var stereo = new StereoBuilder().Build();
    return new MyCar(engine, stereo);
  }
  ...
}

The more we have test data builder classes, the more different objects we can create easily.

Conclusion

I value test data builders a lot and I like to write them. Somehow I like the syntax:

var patient = new MyPatientBuilder()
  .WithFirstName("Lassi").WithLastName("Autio").Build();

Test data builders are one of the keys when I write unit tests, and they make my tests a lot better. Without them, I would have to use much more time writing tests. Their value increases with time. When writing a new test and there is already a test data builder that it can use it is a big win because creating an object is so easy!

Further Readings

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s