Test builder pattern using fluent interface

By | November 23, 2015

This is a brief c# based tutorial on test builder pattern using fluent interface. It is one of the many ways we can tackle the problem of brittle tests. I will try to keep the example as real world as possible. As usual I will deal with the WHY before the HOW. The test builder pattern is actually based on builder pattern described in GoF book. It is sometimes quite useful in code. But it really comes into its own in the world of testing. This was first described by Joshua Bloch in his book “Effective Java”

WHY
Suppose you are unit testing a class. And somehow you wrote a constructor taking too many (read more than 3) parameters.
When you have to unit test the class, you have to create the object. And that is where the problem is.
1. You have to create a proper object, giving valid values to all the fields in it via its constructor. But suppose your test will need a proper value in only one of the fields. Still you are forced to give values to all the fields of the object. In all the tests.
2. Later when the you change/update the constructor signature, your tests fail. You have to manually and change the code creating the object in the tests.

One of reasons developers do not want to re-factor code is the fear of breaking the existing test cases. It is wrong but it happens. This highlights the importance of writing tests which are maintainable. Test builder pattern is one way of getting close to this.

HOW
Its very simple.
1. Create a builder class which creates the objects for you.
2. Provide methods in the builder which allow you to control the way the object fields are populated.

Time for code. (Fast, wasn’t it?)

As usual I will have the Interfaces. There is a bit too much of code here. Do not be scared. There are interfaces and then implementing classes. Only 3 interfaces and 9 implementing classes. For sake of completion I will be listing them all. Feel free to jump down.

The IAirConditioner.cs interface

namespace BuilderDemo
{
    public interface IAirConditioner
    {
        string Rating { get; set; }

        string DisplayRating();

        void Start();
    }
}

implemented by DefaultAirConditioner.cs

using System;

namespace BuilderDemo
{
    public class DefaultAirConditioner : IAirConditioner
    {
        public string Rating { set; get; }

        public DefaultAirConditioner()
        {
            Rating = "0 KW";
        }

        public string DisplayRating()
        {
            return Rating;
        }

        public void Start()
        {
            Console.WriteLine("Imaginary aircon of {0} capacity running", DisplayRating());
        }
    }
}

and by LgAirConditioner.cs

using System;

namespace BuilderDemo
{
    public class LgAirConditioner : IAirConditioner
    {
        public string Rating { set; get; }

        public LgAirConditioner()
        {
            Rating = "20 KW";
        }

        public string DisplayRating()
        {
            return Rating;
        }

        public void Start()
        {
            Console.WriteLine("LG aircon of {0} capacity running", DisplayRating());
        }
    }
}

and by CarrierAirConditioner.cs

using System;

namespace BuilderDemo
{
    public class CarrierAirConditioner : IAirConditioner
    {
        public string Rating { set; get; }

        public CarrierAirConditioner(string brand)
        {
            Rating = "30 KW";
        }

        public string DisplayRating()
        {
            return Rating;
        }

        public void Start()
        {
            Console.WriteLine("Carrier aircon of {0} capacity running", DisplayRating());
        }
    }
}

Similarly we have IWashingMachine.cs

namespace BuilderDemo
{
    public interface IWashingMachine
    {
        string Load { get; set; }

        string DisplayLoad();

        void Start();
    }
}

getting implemented by DefaultWashingMachine.cs

using System;

namespace BuilderDemo
{
    public class DefaultWashingMachine : IWashingMachine
    {
        public string Load { get; set; }

        public DefaultWashingMachine()
        {
            Load = "0 Kg";
        }

        public string DisplayLoad()
        {
            return Load;
        }

        public void Start()
        {
            Console.WriteLine("Imaginary washing machine of {0} capacity running", DisplayLoad());
        }
    }
}

and by MieleWashingMachine

using System;

namespace BuilderDemo
{
    public class MieleWashingMachine : IWashingMachine
    {
        public string Load { get; set; }

        public MieleWashingMachine()
        {
            Load = "15 Kg";
        }

        public string DisplayLoad()
        {
            return Load;
        }

        public void Start()
        {
            Console.WriteLine("Miele washing machine of {0} capacity running", DisplayLoad());
        }
    }
}

and by SamsungWashingMachine

using System;

namespace BuilderDemo
{
    public class SamsungWashingMachine : IWashingMachine
    {
        public string Load { get; set; }

        public SamsungWashingMachine()
        {
            Load = "10 Kg";
        }

        public string DisplayLoad()
        {
            return Load;
        }

        public void Start()
        {
            Console.WriteLine("Samsung washing machine of {0} capacity running", DisplayLoad());
        }
    }
}

There also is the IFridge.cs interface

namespace BuilderDemo
{
    public interface IFridge
    {
        string Capacity { get; set; }

        string DisplayCapacity();

        void Start();
    }
}

which gets implemented by DefaultFridge

using System;

namespace BuilderDemo
{
    public class DefaultFridge : IFridge
    {
        public string Capacity { get; set; }

        public DefaultFridge()
        {
            Capacity = "0 ltrs";
        }

        public string DisplayCapacity()
        {
            return Capacity;
        }

        public void Start()
        {
            Console.WriteLine("Imaginary fridge of {0} capacity running", DisplayCapacity());
        }
    }
}

and by HaierFridge.cs

using System;

namespace BuilderDemo
{
    public class HaierFridge : IFridge
    {
        public string Capacity { get; set; }

        public HaierFridge()
        {
            Capacity = "250 ltrs";
        }

        public string DisplayCapacity()
        {
            return Capacity;
        }

        public void Start()
        {
            Console.WriteLine("Haier fridge of {0} capacity running", DisplayCapacity());
        }
    }
}

and by WhirlpoolFridge.cs

using System;

namespace BuilderDemo
{
    public class WhirlpoolFridge : IFridge
    {
        public string Capacity { get; set; }

        public WhirlpoolFridge()
        {
            Capacity = "210 ltrs";
        }

        public string DisplayCapacity()
        {
            return Capacity;
        }

        public void Start()
        {
            Console.WriteLine("Whirlpool fridge of {0} capacity running", DisplayCapacity());
        }
    }
}


Then we have our class House.cs whose constructor takes the parameters of type washing machine, fridge and air-conditioner.

namespace BuilderDemo
{
    public class House
    {
        public IAirConditioner AirConditioner { get; set; }
        public IFridge Fridge { get; set; }
        public IWashingMachine WashingMachine { get; set; }

        public House(IAirConditioner airConditioner, IFridge fridge, IWashingMachine washingMachine)
        {
            AirConditioner = airConditioner;
            Fridge = fridge;
            WashingMachine = washingMachine;
        }

        public void StartLiving()
        {
            AirConditioner.Start();
            Fridge.Start();
            WashingMachine.Start();
        }
    }
}

Now comes the part where you write test cases for this House.cs class.
I am using xUnit framework here.

[Fact]
public void When_fridge_is_added_to_house_Expect_its_capacity_to_be_correct()
{
    //This is a lame test. I know.
    House sut = new House(new DefaultAirConditioner(), new WhirlpoolFridge(), new DefaultWashingMachine());
    Assert.Equal("210 ltrs", sut.Fridge.Capacity);
}

Notice the problem here. I wanted to test the Fridge field only. But I had to provide some default garbage values to the other fields when I had to create the House object. If there are 500 such cases then you know that it is a pain.
Also notice that if I change the parameter type or count in the constructor of the House.cs class then the test has to change. Parameters have to be updated in all the places where the House.cs class object is getting created. In all those 500 tests. Sounds fun?

Let us change this.
What I will create is a small builder class for this House.cs class. Lets call it HouseBuilder.cs and here it is

namespace BuilderDemo
{
    public class HouseBuilder
    {
        private IAirConditioner _airConditioner;

        private IFridge _fridge;

        private IWashingMachine _washingMachine;

        public HouseBuilder()
        {
            _airConditioner = new DefaultAirConditioner();
            _fridge = new DefaultFridge();
            _washingMachine = new DefaultWashingMachine();
        }

        public HouseBuilder WithAirConditioner(IAirConditioner airConditioner)
        {
            _airConditioner = airConditioner;
            return this;
        }

        public HouseBuilder WithFridge(IFridge fridge)
        {
            _fridge = fridge;
            return this;
        }

        public HouseBuilder WithWashingMachine(IWashingMachine washingMachine)
        {
            _washingMachine = washingMachine;
            return this;
        }

        public House Build()
        {
            return new House(_anotherParameterNotAgain, _airConditioner, _fridge, _washingMachine);
        }
    }
}

It is very simple but it gets the job done.
Line 6 : This is a parameterless constructor which internally puts some default values in all the fields the constructor of the House.cs class needs.
Line 34 : This is the method which actually creates an object of type House.cs and returns the same.
Line 16 : This is where the builder does its magic of chaining the calls. It takes a parameter of the type and assigns it to the field inside it. And note the return type. It allows the method chaining which is so needed for Fluent syntax.

As a result of this our test case now has changed!!!

[Fact]
public void Improved_When_fridge_is_added_to_house_Expect_its_capacity_to_be_correct()
{
    //This is a lame test. But better.
    House sut = new HouseBuilder().WithFridge(new WhirlpoolFridge()).Build();
    Assert.Equal("210 ltrs", sut.Fridge.Capacity);
}

Immediately one benefit will jump out. The parameter which is crucial to my testing is the one getting passed explicitly. The builder is taking care of passing the default values to the fields which my test is not really concerned with. If this constructor had 12 parameters (Don’t do it) then this is a big big win.

Another thing which might have skipped your attention is the readability.

Mobile mobile = new Mobile().WithGSMSupport(true).WithDualSim(true).WithBuiltInRadio(false);

is more readable than

Mobile mobile = new Mobile(true,true,false);

Yet another benefit is this. Suppose for some weird reasons you just need a object of House class. But you are not going to do anything with it. Just one particular silly function needs a House class object. In that case all you need to do is to create an object with default values. With test builder pattern this is particularly easy.

House sut = new HouseBuilder().Build();

But the greatest benefit is this. Suppose you being the evil you are, added one more parameter to the constructor of the House.cs class. And your test cases change.

[Fact]
public void When_fridge_is_added_to_house_Expect_its_capacity_to_be_correct()
{
    //This is a lame test. I know.
    House sut = new House(new AnotherParameterNotAgain(), new DefaultAirConditioner(), new WhirlpoolFridge(), new DefaultWashingMachine());
    Assert.Equal("210 ltrs", sut.Fridge.Capacity);
}

All 500 of them. Yikees !!!!

But the story is different with the builder.
You just add the new field to the builder HouseBuilder.cs like this

namespace BuilderDemo
{
    public class HouseBuilder
    {
        private IAirConditioner _airConditioner;

        private IFridge _fridge;

        private IWashingMachine _washingMachine;

        private IAnotherParameterNotAgain _anotherParameterNotAgain;

        public HouseBuilder()
        {
            _airConditioner = new DefaultAirConditioner();
            _fridge = new DefaultFridge();
            _washingMachine = new DefaultWashingMachine();
            _anotherParameterNotAgain = new AnotherParameterNotAgain();
        }

        public HouseBuilder WithAirConditioner(IAirConditioner airConditioner)
        {
            _airConditioner = airConditioner;
            return this;
        }

        public HouseBuilder WithFridge(IFridge fridge)
        {
            _fridge = fridge;
            return this;
        }

        public HouseBuilder WithWashingMachine(IWashingMachine washingMachine)
        {
            _washingMachine = washingMachine;
            return this;
        }

        public House Build()
        {
            return new House(_anotherParameterNotAgain, _airConditioner, _fridge, _washingMachine);
        }
    }
}

And the test case? Does it get changed? NO

public void Improved_When_fridge_is_added_to_house_Expect_its_capacity_to_be_correct()
{
    //This is a lame test. I know.
    House sut = new HouseBuilder().WithFridge(new WhirlpoolFridge()).Build();
    Assert.Equal("210 ltrs", sut.Fridge.Capacity);
}

Of course you will be writing new unit tests for the newly introduced parameter. But the key point is that the existing tests were still working. Nothing broken.

And you as a developer now have one reason less not to refactor your code to make it better.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.