We don't need no steenkin' Test Data Builders!

This is the fifth and final in a series of articles about the relationship between the Test Data Builder design pattern, and the identity functor. In the previous article, you learned why a Builder functor adds little value. In this article, you'll see what to do instead.

From Identity to naked values

While you can define Test Data Builders with Haskell's Identity functor, it adds little value:

Identity address = fmap (\-> a { city = "Paris" }) addressBuilder

That's nothing but an overly complicated way to create a data value from another data value. You can simplify the code from the previous article. First, instead of calling them 'Builders', we should be honest and name them as the default values they are:

defaultPostCode :: PostCode
defaultPostCode = PostCode []
 
defaultAddress :: Address
defaultAddress  = Address { street = "", city = "", postCode = defaultPostCode }

defaultPostCode is nothing but an empty PostCode value, and defaultAddress is an Address value with empty constituent values. Notice that defaultAddress uses defaultPostCode for the postCode value.

If you need a value in Paris, you can simply write it like this:

address = defaultAddress { city = "Paris" }

Likewise, if you need a more specific address, but you don't care about the post code, you can write it like this:

address' =
  Address { street = "Rue Morgue", city = "Paris", postCode = defaultPostCode }

Notice how much simpler this is. There's no need to call fmap in order to pull the 'underlying value' out of the functor, transform it, and put it back in the functor. Haskell's 'copy and update' syntax gives you this ability for free. It's built into the language.

Building F# values

Haskell isn't the only language with 'copy and update' syntax. F# has it as well, and in fact, it's from the F# documentation that I've taken the 'copy and update' term.

The code corresponding to the above Haskell code looks like this in F#:

let defaultPostCode = PostCode []
let defaultAddress = { Street = ""; City = ""; PostCode = defaultPostCode }
 
let address = { defaultAddress with City = "Paris" }
let address' =
    { Street = "Rue Morgue"; City = "Paris"; PostCode = defaultPostCode }

The syntax is a little different, but the concepts are the same. F# adds the keyword with to 'copy and update' expressions, which translates easily back to C# fluent interfaces.

Building C# objects

In a previous article, you saw how to refactor your domain model to a model of Value Objects with fluent interfaces.

In your unit tests, you can define natural default values for testing purposes:

public static class Natural
{
    public static PostCode PostCode = new PostCode();
    public static Address Address = new Address("""", PostCode);
    public static InvoiceLine InvoiceLine =
        new InvoiceLine(""PoundsShillingsPence.Zero);
    public static Recipient Recipient = new Recipient("", Address);
    public static Invoice Invoice = new Invoice(Recipient, new InvoiceLine[0]);
}

This static Natural class is a test-specific container of 'good' default values. Notice how, once more, the Address value uses the PostCode value to fill in the PostCode property of the default Address value.

With these default test values, and the fluent interface of your domain model, you can easily build a test address in Paris:

var address = Natural.Address.WithCity("Paris");

Because Natural.Address is an Address object, you can use its WithCity method to build a test address in Paris, and where all other constituent values remain the default values.

Likewise, you can create an address on Rue Morgue, but with a default post code:

var address = new Address("Rue Morgue""Paris"Natural.PostCode);

Here, you can simply create a new Address object, but with Natural.PostCode as the post code value.

Conclusion

Using a fluent domain model obviates the need for Test Data Builders. There's a tendency among functional programmers to overbearingly state that design patterns are nothing but recipes to overcome deficiencies in particular programming languages or paradigms. If you believe such a claim, at least it ought to go both ways, but at the conclusion of this article series, I hope I've been able to demonstrate that this is true for the Test Data Builder pattern. You only need it for 'classic', mutable, object-oriented domain models.

  1. For mutable object models, use Test Data Builders.
  2. Consider, however, modelling your domain with Value Objects and 'copy and update' instance methods.
  3. Even better, consider using a programming language with built-in 'copy and update' expressions.
If you're stuck with a language like C# or Java, you don't get language-level support for 'copy and update' expressions. This means that you'll still need to incur the cost of adding and maintaining all those With[...] methods:

public class Invoice
{
    public Recipient Recipient { get; }
    public IReadOnlyCollection<InvoiceLine> Lines { get; }
 
    public Invoice(
        Recipient recipient,
        IReadOnlyCollection<InvoiceLine> lines)
    {
        if (recipient == null)
            throw new ArgumentNullException(nameof(recipient));
        if (lines == null)
            throw new ArgumentNullException(nameof(lines));
 
        this.Recipient = recipient;
        this.Lines = lines;
    }
 
    public Invoice WithRecipient(Recipient newRecipient)
    {
        return new Invoice(newRecipient, this.Lines);
    }
 
    public Invoice WithLines(IReadOnlyCollection<InvoiceLine> newLines)
    {
        return new Invoice(this.Recipient, newLines);
    }
 
    public override bool Equals(object obj)
    {
        var other = obj as Invoice;
        if (other == null)
            return base.Equals(obj);
 
        return object.Equals(this.Recipient, other.Recipient)
            && Enumerable.SequenceEqual(
                this.Lines.OrderBy(l => l.Name),
                other.Lines.OrderBy(l => l.Name));
    }
 
    public override int GetHashCode()
    {
        return
            this.Recipient.GetHashCode() ^
            this.Lines.GetHashCode();
    }
}

That may seem like quite a maintenance burden (and it is), but consider that it has the same degree of complexity and overhead as defining a Test Data Builder for each domain object. At least, by putting this extra code in your domain model, you make all of that API (all the With[...] methods, and the structural equality) available to other production code. In my experience, that's a better return of investment than isolating such useful features only to test code.

Still, once you've tried using a language like F# or Haskell, where 'copy and update' expressions come with the language, you realise how much redundant code you're writing in C# or Java. The Test Data Builder design pattern truly is a recipe that addresses deficiencies in particular languages.

Next: The Test Data Generator functor.


Comments

Hi Marks, thanks for the whole serie. I personally tend to split my class into 2: 'core' feature and syntactic sugar one.
Leveraging extension methods to implement 'With' API is relatively straightforward and you have both developper friendly API and a great separation of concern namely definition and usage.
If you choose to implement extensions in another assembly you could manage who have access to it: unit test only, another assembly, whole project.
You can split API according to context/user too. It can also be useful to enforce some guidelines.
2017-09-12 09:20 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Monday, 11 September 2017 07:28:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!