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; }

    Product Product { 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.

Other articles in the series

Share




  • Harry McIntyre

    Hi Vladimir, good series of posts.

    I have a ‘best practice’ I’m trying to promote. Rather than using //Arrange //Act //Assert as comments, write Gherkin //Given //When //Then comment, describing the test in behavioural terms.

    Example at http://10printhello.com/the-one-bdd-framework-to-rule-them/

    I find doing this helps

    – explain the tests purpose better than reading the class names
    – distinguish which tests are testing a feature vs an implementation detail (i.e. can this test be dropped if we are changing the architecture, or will we need to re-implement it after the change to ensure the system is still functional)
    – protects the test’s meaning against automated refactoring

    Thoughts?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Hi Harry,

      The Gherkin style is indeed a good one, I use both AAA and GWT on my projects.

      I personally find AAA good for simple situations (which make up the vast majority of cases), whereas the Given-When-Then approach is helpful when a complex set of tests for a single SUT is needed. It helps simplify tests and preserve their expressiveness on large test suites.

      • Harry McIntyre

        I guess what I was saying was that religiously sticking to GWT comments, even for the seemingly simple cases, has real benefits in the long run, and means that I never fail to document what turns out to be a helpful test.

        For me, a large test pack of AAA (or uncommented) unit tests becomes a hindrance to architectural change.

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          Interesting points. I probably need to give it a try (sticking to GWT even in simple cases) and see how it goes.

          I like the idea that tests become better documented by design.

  • Randy A MacDonald

    Don’t agree with the use TDD iff exact knowledge, at least as far as reverting to code-first. If you’re writing any code, you surely have some idea as to why, and you would be surprised to see how easy that why becomes a test. It is probably true you don’t have exact knowledge of a big system immediately, no one could sensibly expect that, but there is something that can be said.

    I’m also reminded of the trick to get you to decide on something: flip a coin, and your hope may appear as the coin falls. The outcome when the coin settles may solidify your feelings about it.