Encapsulating Test Data and Expectations


I love it when I find new ways to improve my testing ability.  In this case, it’s not really new, just new to me.  I’m referring The Object Mother or Test Data Builder patterns used to encapsulate objects you need for testing.  I started playing with these patterns to simplify my tests and I found a way to further reduce the noise in my tests.

The basic concepts of these concept of these patterns is to put object populated with what you need behind simple factory methods to give you back the objects.  Test Builders can be more complicated, perhaps using a fluent interface to define the object exactly as you need for particular test.  The complexity for most of my tests fell somewhere between these too approaches.  The factory methods didn’t give me enough flexibility and most of my objects were not complex enough to warrant a full fluent interfaces.

Most of my tests seem to follow this pattern, where I define variables to hold my expected values, assign these values to the objects I need to test and then run my assertions against these expected values.

And then do my tests:

private string _expectedName = "name";
private string _expectedPhone = "phone";
private string _expectedPlus4 = "plus 4";
private string _expectedStateAbbrev = "state";
private string _expectedZip = "zip";
...
[Test]
public void should_populate_the_work_order_table_to_the_work_order_dto()
{
    
    WorkOrderMapper mapper = new WorkOrderMapper();
    DataSet workOrderDataSet = SetUpWorkOrderDataSet();
    var dto = mapper.MapFrom(workOrderDataSet);
    Assert.That(dto.CompanyName, Is.EqualTo(_expectedCompanyName));
    Assert.That(dto.Phone, Is.EqualTo(_expectedPhone));
    Assert.That(dto.StateAbbrev, Is.EqualTo(_expectedStateAbbrev));
    Assert.That(dto.ZipCode, Is.EqualTo(_expectedZip));
}

So I once I started exploring the Object Mother pattern, I realized that I could encapsulate not only the data I need for my objects, but also my expectation variables as well.  So I created TestData objects, where I declared the expected values and populated the objects with those values.  After a couple of different approaches, these test data classes started to look like this.

public class CustomerTestData
{
    public CustomerTestData()
        : this(null)
    {
    }
public ExpectationData Expectations { get;set;} 
public Customer Item{get;set;} 
    public class ExpectationData
    {
        public int Id = 0;
        public string CustNo = "CustNo";
        public string Company = "Company";
        public double Tax = 50;
        public double Discount = 10;
        public string GlLink = "gl";
        public bool IsTaxExempt = true;
        ...
        public ExpectationData()
        {
        }
   }
}

I could then use the Test Data Objects in my test like this:

[Test] 
public void should_sum_all_line_item_prices_times_quantity()
{
    var salesOrder = TestingData.GetSalesOrderTestData().SalesOrder;
    
    var actualTotal = salesOrder.GetOrderTotal();
    Assert.That(actualTotal, Is.EqualTo(50m));
    
}

Variability and Keeping Test Readable

There are two issues with this approach.  The first one you probably noticed right away.  There’s no way the same data can be used to satisfy all of your tests.  What if I have tests that need a null value or an invalid value?  Having the Expectations pre-defined reduce their usability to only positive tests.  The other issue is a little more subtle, but this approach reduces the readability of my test.  Now when someone wants to know what my tests are doing they have to look in two places, the test itself and the TestData objects.  The reduces their overall usefulness in describing the behavior of the system and it’s just plain annoying.

So what we need is the ability to define the values that are important to the test, but also create the objects in a valid state with default values unrelated to the test at hand.  We can do this with an Action delegate.  Lets create the TestData with an Action delegate:

public CustomerTestData(Action<CustomerTestData.ExpectationData> overrideDefaultExpecations)
{
    Expectations = new CustomertTestData.ExpectationData();
    Item = Expectations.SetExpectionsOn(overrideDefaultExpecations);
}

Since delegates are first class citizens in .Net, we can pass that function around and execute when we need it.  Let’s create a method in ExpectationData class the creates our Customer object.  Now we’ll use the delegate to change the expected values.  This will change the value of the expectations before we set the values on the Customer object.

public Customer SetExpectionsOn(Action<CustomerTestData.ExpectationData> setExpectations)
{
    if (setExpectations != null)
    {
        setExpectations(this);
    }
    ArCust item = new ArCust();
    item.Id = Id;
    item.Company = Company;
    ...
    return item;

}

Now are our test looks like this:

[Test] public void should_sum_all_line_item_prices_times_quantity()
{
      var salesOrder = TestingData.GetSalesOrderTestData(e => {
                                                                                                  e.LineItem.Price = 10m;
                                                                                                  e.LineItem.Price = 5m;
                                                                                                 }).SalesOrder;
      var actualTotal = salesOrder.GetOrderTotal();
     Assert.That(actualTotal, Is.EqualTo(50m));
}

Now we can set the values for the parameters we are interested in, keeping our test objects in a valid state and maintaining the readability needed to understand the test without needing to refer to other object.

Conclusion

I’m still tweaking the syntax, but so far it has really cleaned up some of my tests.  However, it can fall apart if you have a complicated setup process.  Some of my tests require the same data in multiple objects, so that that they all  match when my service is executed.  Using the delegates to wire up 3 different objects in the setup became very difficult to read.  So I switched to the Builder pattern (and some refactoring) to clean up the process.  In other cases I needed a quick method to populate all the fields, so I wrapped it up in a factory method.  It is very easy to combine this approach with Builders and Factory Methods to give you as much control as you need over your test data.

Iteration 0