TDD best practices

Last week, we discussed the differences between stubs and mocks. Today, we’ll talk about some general tips and advice that regard to TDD and writing tests in general.

Test-first vs code-first approach

There’s some dispute about how exactly tests should be written. While the TDD process itself insists on the test-first approach, I personally think that both ways are applicable in different circumstances. Let’s look at the pros and cons of each of them.

One of the benefits of the test-first approach is that it allows us to easily validate the tests we create. If we follow the red-green-refactor routine, the first step would always be creating a "red" test and thus making sure it’s actually exposing problems with our code.

With the code-first approach, unit tests are prone to false positives. As we create them after the implementation is done, there’s no guarantee these tests actually do their job. In order to verify that, we often need to manually change the implementation of the SUT (system under test) to make sure the test fails. With the test-first approach, this check is already wired into the red-green-refactor process.

Another great advantage the test-first approach provides us is the ability to sketch our thoughts out. Following this way, we can create several failing tests and thus make ourselves a road map of what is needed to be done. It’s especially helpful in an environment where you are constantly distracted. This approach allows you to not forget where you had stopped and easily return to the flow. It is also a nice source of endorphins: watching your tests turning green one by one is always a pleasure.

On the other hand, the code-first approach is helpful when you need to investigate some new areas in your application. Also, the TDD process is often redundant when building a prototype. Most likely, such prototype will be deleted along with the tests covering it.

I believe TDD does a great job when you know the complete set of requirements for the code you are going to write. In such cases, the test-first approach would indeed be the best choice.

At the same time, if you are experimenting with different ideas in code or not exactly sure how your code should look like, you are better off not trying to come up with unit tests up-front. Instead, just write the code and add unit tests later. Also, sometimes it’s reasonable to switch between the two approaches, even within a single piece of functionality.

Test doubles are for external (volatile) dependencies only

While using stubs is a good practice when you don’t have control over some dependencies in your code, I often see developers substituting even internal (stable) dependencies. Usually, the reason for that is isolation. In order to isolate a class from other classes completely, people sometimes try to mock even those of them that comprise a cohesive group with the system under test.

Let’s take an example:

public class Order
{
    public decimal Amount
    {
        get { return _lines.Sum(x => x.Price); }
    }
 
    public void AddLine(IOrderLine line)
    {
        _lines.Add(line);
    }
}

public interface IOrderLine
{
    decimal Price { get; set; }
    ProductProduct { get; set; }
}

[Fact]
public void Amount_shows_order_amount()
{
    // Arrange
    var mock = new Mock<IOrderLine>();
    mock.Setup(x => x.Price).Returns(10);
    var mock2 = new Mock<IOrderLine>();
    mock2.Setup(x => x.Price).Returns(20);
    var order = new Order();
    order.AddLine(mock.Object);
    order.AddLine(mock2.Object);
 
    // Act
    decimal amount = order.Amount;
 
    // Assert
    Assert.Equal(30, amount);
}

Here, we use IOrderLine interface in the Order class and mock it in the unit test.

What is the problem with such type of isolation? It loosens cohesion between the Order and the OrderLine classes. We shouldn’t break our code in places where it doesn’t break. It this particular situation, it would be better to use OrderLine as is, without trying to substitute it with an interface:

public class Order
{
    // Other methods
 
    public void AddLine(decimal price, Product product)
    {
        _lines.Add(new OrderLine(price, product));
    }
}

[Fact]
public void Amount_shows_order_amount()
{
    // Arrange
    var order = new Order();
    order.AddLine(10, new Product());
    order.AddLine(20, new Product());
 
    // Act
    decimal amount = order.Amount;
 
    // Assert
    Assert.Equal(30, amount);
}

That way, we keep our domain model cohesive and don’t introduce unnecessary seams in our code.

A single unit for unit-testing is an aggregate

Another best practice for unit testing a domain model is treating aggregates in it as a single SUT. OrderLine in the previous example is a child entity of the Order aggregate. We shouldn’t try to test it separately as it would break the aggregate’s encapsulation. A better way would be to consider both the Order and the OrderLine classes a single unit and test them respectively.

Sharing code between test classes

Another rule that I think is worth following is the one that regards to sharing code between test classes.

There are two types of the code in unit tests that could potentially be extracted and reused over several test classes: arranges and assertions.

Assertions is a good candidate for that. An example here could be a hand-written stub/spy (you can look at the HandlerStub from the previous post) which provides methods validating its internal state or a utility class with extension methods that help simplify the assertions.

We could use such utility class to replace this code:

Assert.Equal(1, user.DomainEvents.Count);
Assert.True(user.DomainEvents.Any(x => x.GetType() ==typeof(SeatDeletedEvent)));

With a less verbose one-liner:

user.ShouldContainSingleEvent<SeatDeletedEvent>();

Fluent Assertions library goes even further and offers a reusable set of extension methods that simplifies assertions for most of the basic BCL types.

At the same time, code in Arrange sections isn’t something that can be reused as freely. It might seem a good idea to gather all arrange methods in a base test class and use them in every unit test:

[Fact]
public void Fires_event_after_provisioning()
{
    // Arrange (methods are in the TestBase class)
    Organization organization = CreateOrganization();
    User user = CreateUser(organization);
    Subscription subscription = CreateSubscription(organization);
 
    // Act
    organization.ProvisionUser(user, subscription);
 
    // Assert
    user.ShouldContainSingleEvent<SeatCreatedEvent>();
}

But this would introduce too much of fragility. It’s really hard to maintain tests that depend on each other: any change in one of such methods can potentially break all tests that rely on it.

At the same time, it isn’t a good idea to give up on reusability in the arrange section completely. Just as in any field of software development, there should be a balance here.

I personally tend to not use base classes in my unit tests and thus, not share the arrange code between test classes. However, I do share it inside a single test class. In the example above, Create methods are stored as private methods inside the UserSpecs class and reused across its test methods.

I believe it’s a reasonable trade-off between the reusability and the fragility of the code. It does lead to some duplications, though. For example, UserSpecs and OrganizationSpecs might have the same CreateOrganization method.

In test classes, using composition instead of inheritance helps reduce coupling between them.

If you enjoyed this article, be sure to check out my Pragmatic Unit Testing Pluralsight course too.

Summary

Let’s recap:

  • Use test-first (TDD) approach if you know exactly what you want your code to do. Use code-first approach otherwise.

  • Use test doubles only to substitute external dependencies.

  • A single SUT in your domain model should be an aggregate.

  • Reuse code in the Assert sections.

  • Limit the code reuse in the Arrange sections.

  • Prefer composition over inheritance in your test classes.

That wraps up this article series. It turned out to be twice longer than I initially excepted. I hope you found it interesting.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus