How to do painless TDD

Last week, we nailed the root cause of the problems, related to so-called test-induced damage - damage we have to bring into our design in order to make the code testable. Today, we’ll look at how we can mitigate that damage, or, in other words, do painless TDD.

Painless TDD: to mock or not to mock?

So is it possible to not damage the code while keeping it testable? Sure it is. You just need to get rid of the mocks altogether. I know it sounds like an overstatement, so let me explain myself.

Software development is all about compromises. They are everywhere: CAP theorem, Speed-Cost-Quality triangle, etc. The situation with unit tests is no different. Do you really need to have 100% test coverage? If you develop a typical enterprise application, then, most likely, no. What you do need is a solid test suite that covers all business critical code; you don’t want to waste your time on unit-testing trivial parts of your code base.

Okay, but what if you can’t test a critical business logic without introducing mocks? In that case, you need to extract such logic out of the methods with external dependencies. Let’s look at the method from the previous post:

public class CustomerController : ApiController
{
    private readonly ICustomerRepository_repository;
    private readonly IEmailGateway_emailGateway;
 
    public CustomerController(ICustomerRepository repository,
        IEmailGateway emailGateway)
    {
        _emailGateway = emailGateway;
        _repository = repository;
    }
 
    [HttpPost]
    public HttpResponseMessage CreateCustomer([FromBody] string name)
    {
        Customer customer = new Customer();
        customer.Name = name;
        customer.State = CustomerState.Pending;
 
        _repository.Save(customer);
        _emailGateway.SendGreetings(customer);
 
        return Ok();
    }
}

What logic is really worth testing here? I argue that only the first 3 lines of the CreateCustomer method are. Repository.Save method is important, but mocks don’t actually help us ensure it works, they just verify the CreateCustomer method calls it. Same is for the SendGreetings method.

So, we just need to extract those lines in a constructor (or, if there would be some more complex logic, in a factory):

[HttpPost]
public HttpResponseMessage CreateCustomer([FromBody] string name)
{
    Customer customer = new Customer(name);
 
    _repository.Save(customer);
    _emailGateway.SendGreetings(customer);
 
    return Ok();
}
public class Customer
{
    public Customer(string name)
    {
        Name = name;
        State = CustomerState.Pending;
    }
}

Unit-testing of those lines becomes trivial:

[Fact]
public void New_customer_is_in_pending_state()
{
    var customer = new Customer("John Doe");
 
    Assert.Equal("John Doe", customer.Name);
    Assert.Equal(CustomerState.Pending, customer.State);
}

Note that there’s no arrange section in this test. That’s a sign of highly maintainable tests: the fewer arrangements you do in a unit test, the more maintainable is becomes. Of course, it’s not always possible to get rid of them completely, but the general rule remains: you need to keep the Arrange section as small as possible. In order to do this, you need to stop using mocks in your unit tests.

But what about the controller? Should we just stop unit-testing it? Exactly. The controller now doesn’t contain any essential logic itself, it just coordinates the work of the other actors. Its logic became trivial. Testing of such logic brings more costs than profits, so we are better off not wasting our time on it.

Types of code

That brings us to some interesting conclusions regarding how to make TDD painless. We need to unit test only a specific type of code which:

  • Doesn’t have external dependencies.

  • Expresses your domain.

By external dependencies, I mean objects that depend on the external world’s state. For example, repositories depend upon data in the database, file managers depend upon files in the files system, etc. Mark Seemann in his book Dependency Injection in .NET describes such dependencies as volatile. Other types of dependencies (strings, DateTime, or even domain classes) don’t count as such.

Here’s how we can illustrate different types of code:

Types of code to test
Types of code to test

Steve Sanderson wrote a brilliant article on that topic, so you might want to check it out for more details.

Generally, the maintainability cost of the code that both contains domain logic and has external dependencies (the "Mess" quadrant at the diagram) is too high. That what we had in the controller in the beginning: it depended on external dependencies and did some domain-specific work at the same time. The code like that should be split up: the domain logic should be placed in the domain objects so that the controller keeps track of coordination and putting it all together.

From the remaining 3 types of code in your code base (domain model, trivial code, and controllers), you need to unit test only the domain-related code. That is where you can have the best return of your investments; that is a trade-off I advocate you make.

Here are my points on that:

  • Not all code is equally important. Your application contains some business-critical parts (the domain model), on which you should focus the most of your efforts.

  • Domain model is self-contained, it doesn’t depend on the external world. It means that you don’t need to use any mocks to test it, nor should you. Because of that, unit tests aimed to test your domain model are easy to implement and maintain.

Following these practices helps build a solid unit test suite. The goal of unit-testing is not 100% test coverage (although in some cases it is reasonable). The goal is to be confident that changes and new code don’t break existing functionality.

I stopped using mocks long time ago. Since then, not only did the quality of the code I write not dropped, but I also have a maintainable and reliable unit test suite that helps me evolve code bases of the projects I work on. TDD became painless for me.

You might argue that even with these guidelines, you can’t always get rid of the mocks in your tests completely. An example here may be the necessity of keeping customers' emails unique: we do need to mock the repository in order to test such logic. Don’t worry, I’ll address this case (and some others) in the next article.

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

Summary

Let’s summarize this post with the following:

  • Don’t use mocks

  • Extract the domain model out of methods with external dependencies

  • Unit-test only the domain model

In the next post, we’ll discuss integration testing and how it helps us in the cases where unit tests are helpless.

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