Reusing code in tests

When writing tests, you can often end up with tests that have (almost) the same setup. Certain input variables may change, but the structure of the setup code remains the same.

When the setup is small, you can copy from one test to another. But when there is more ceremony involved, you will start to feel the need to avoid duplication.

It's a best practice to apply coding principles to both your production code and your test code. Don't repeat yourself (DRY) is one of those principles.

Let's have a look at an example. We're going to test a class that sends a mail to a customer.

[TestFixture]
public class CustomerMailServiceTest  
{
    [Test]
    public void Send_ForNormalCustomer_ShouldCallMailServer()
    {
        var mailServerMock = new Mock<IMailServer>();
        var customer = new Customer
        {
            FirstName = "John",
            LastName = "Doe",
            Email = "[email protected]"
        }

        var service = new CustomerMailService(mailServerMock.Object);

        service.Send(customer);

        mailServerMock.Verify(x => x.SendMail());
    }
}

Never mind that this made-up example doesn't make one hundred percent sense, and that the setup is really small. It's just an example, but in production code, there is often much more setup needed, before you can call the method you want to test.

Taking the example, let's say you also need to test:

  • A customer without FirstName
  • A customer without LastName
  • A customer without Email
  • A customer with an invalid Email
  • The IMailServer instance throwing an Exception

In a first attempt, you could copy the code and change the specifics. In the above example, that would be ok (to me), but with more setup code, this would feel dirty.

Inheritance to the rescue, sort of

A solution I've seen and used before is to use inheritance. You could make a base class like this:

public abstract class GivenACustomer  
{
    protected abstract FirstName { get; }
    protected abstract LastName { get; }
    protected abstract Email { get; }
    protected Customer Customer { get; private set; }

    [SetUp]
    protected void Setup()
    {
        var mailServerMock = new Mock<IMailServer>();
        Customer = new Customer
        {
            FirstName = FirstName,
            LastName = LastName,
            Email = Email
        }
    }
}

Your test then simply becomes:

[TestFixture]
public class GivenANormalCustomer : GivenACustomer  
{
    protected override string FirstName => "John";
    protected override string LastName => "Doe";
    protected override string Email => "[email protected]";

    [Test]
    public void Send_ShouldCallMailServer()
    {
        var mailServerMock = new Mock<IMailServer>();

        var service = new CustomerMailService(mailServerMock.Object);

        service.Send(customer);

        mailServerMock.Verify(x => x.SendMail());
    }
}

You can now easily add a test for a Customer without a FirstName, without a LastName, etc.

Again, the setup code here was minimal, so we haven't won a lot, but if your tests have more pieces to arrange before they can test the system, you will make some gains with this technique.

Multiple levels of inheritance

My issue with this is the following. I believe inheritance should only be used in an is-a relationship, not for pure code reuse.

This means I'll probably only ever use it in a domain, when modeling the outside world. For example, when you have an Employee as a base class and an inheriting Manager class.

But too often, inheritance hinders your ability to introduce changes. Especially when you start having multiple layers of inheritance, because you have an increasing need for edge cases.

This is why the term "composition over inheritance" was coined. We'll get into that in just a bit.

What does this all mean for your tests? When going down the inheritance route, your tests will be harder to maintain, and harder to read. Especially when you have multiple layers of inheritance, you often need to navigate up the inheritance chain, to see what's actually going on.

You might say this means your base classes don't have descriptive names, but when your setup becomes too complex, your class names won't be able to cover what's going on in those base class. Or you will need ridiculously long class names.

One last drawback of using inheritance in your tests, is that it makes it harder to reuse pieces in other tests. Say you have a base class setting up a Customer, a Vendor and a ShopLocation. What if you have a test that only needs a Customer? Or a Customer and a Vendor, but not a ShopLocation? Or a Customer and an Inventory? You would have to make an n-dimensional matrix and create a real inheritance-hell to achieve that.

Builders to the rescue

So, the inheritance approach doesn't feel right at best, and at worst, it kills our ability to write clear, maintainable and readable tests.

I suggest a different approach. Mind you, this isn't my invention, but rather an approach I would like to convince you of. The idea is to create builders when you notice you need certain pieces in tests often.

Let's rewrite our example, using a builder:

[TestFixture]
public class CustomerMailServiceTest  
{
    [Test]
    public void Send_ForNormalCustomer_ShouldCallMailServer()
    {
        var mailServerMock = new Mock<IMailServer>();
        var customer = new CustomerBuilder()
            .WithFirstName("John")
            .WithLastName("Doe")
            .WithEmail("[email protected]")
            .Build();

        var service = new CustomerMailService(mailServerMock.Object);

        service.Send(customer);

        mailServerMock.Verify(x => x.SendMail());
    }
}

The exact approach of the builder is up to you. You could also just have a method CreateValidCustomer, for example. It's basically what you feel is best for your needs.

But the point should be clear. This CustomerBuilder is a class that can be used in any test (loose coupling) and does one thing only (single responsibility). If the needs to create a valid customer change, you can also change it in one place, and all your tests will profit.

I hope you see the benefits of using builders over using inheritance. In essence, you're applying the composition over inheritance pattern here, more or less. Instead of reusing code by inheriting, often from multiple (layered) base-classes, you're pulling in only what you need. You have a greater degree of flexibility to change or add requirements to your tests. So it allows you to more easily apply best practices to your tests.

Know when to stop

Let's stop to look at what we're trying to avoid here. The problem was our test were too large and required too much setup code, making them hard to maintain and read.

We have a technique to mitigate this, but maybe we should ask ourselves if the system, class or method we're testing, isn't doing too much? Or maybe a classic test in C# isn't the best way to test this?

If you find yourself writing immensely large tests, setting up all kinds of preconditions, you might benefit from using another framework than the typical ones we know and love (i.e. NUnit, xUnit, MSTest,...). This is often the case with integration tests and end-to-end tests.

In that case, I recommend you take a look at something like SpecFlow. It allows you to create components in C# that you can reuse in easy-to-read tests, written in Markdown. This also makes the test more readable for non-technical users.

Conclusion

Whenever I see inheritance, a small red flag goes up. It requires some more investigation, but I have rarely seen cases where it doesn't become a blocking issue later on. Especially when it is done purely for code reuse.

As an independent consultant, I focus on turning around legacy projects, making them maintainable again. And one of the big blocks I often have to tackle, is reducing or removing too many pieces of inheritance.

Don't get me wrong, inheritance has its place. But I always think twice, to see if there isn't any other way. When modeling a domain, this is often not the case, but in all other pieces of code, there is usually a better technique. And tests are no different than production code.