If Tests Could Talk, What Would They Say About Your Code?
If Tests Could Talk, What Would They Say About Your Code?

If Tests Could Talk, What Would They Say About Your Code?

From the day you are born, there is one thing that follows you through your whole life.

And that thing is a story.

Good ones, bad ones, scary ones.

A story about how Cinderella has beaten all the odds of life to get the prince charming.
A story about Snow White and her seven sidekicks.
A story about Little Red Riding Hood and a big bad wolf.

In adulthood, you replace these stories with a good movie or a TV show. Which is essentially a story, but someone gets paid to act in it.

But what about the stories in the code?

If tests could talk, what would they tell you about the code they are checking?
Maybe they wouldn’t say anything, meaning there are no tests in the code. Other times they scream at you to improve the underlying code.

Let’s go through some code examples to find out more.

Story Number 1 – What’s the meaning of life? What’s the meaning of a test’s existence?

Code is like a box of chocolates. You never know what you’re gonna test.

Forrest Gump, 28 years, Junior Developer at Nike

One of the most philosophical questions any person can ask is, “What is the meaning of life?
Before we answer this question once and for all, let’s look at the following class.

public class AccountService
{
    private IAccountRepository _accountRepository;

    public AccountService(IAccountRepository accountRepository)
    {
        _accountRepository = accountRepository;
    }

    public void Save(Account account)
    {
        _accountRepository.SaveAccountToDatabase(account);
    }


    public void Update(Account account)
    {
        _accountRepository.UpdateAccount(account);
    }
}

AccountService is a simple class whose only purpose in this world is to delegate method calls. For example, if you look at the Save method, you will notice that the method passes a call to the IAccountRepository.
Let’s look at the test.

[Fact]
public void Save_calls_repository_save_to_database()
{
    //Arrange
    var repositoryMock = new Mock<IAccountRepository>();
    var sut = new AccountService(repositoryMock.Object);
    //Act
    sut.Save(new Account());
    //Assert
    repositoryMock
    .Verify(x => x.SaveAccountToDatabase(It.IsAny<Account>()), Times.Once);
}

It uses Moq to check that the AccountService calls the SaveAccountToDatabase method.

The test is simple. Almost too simple.

Look, unit tests should be small, simple, and fast. But this simple test tells you that the underlying code is too simple to be tested. If it’s too simple to be tested, maybe it should not exist at all?

Now, what do I mean by that? (And what happened to the promise that I will answer the question about the meaning of life? No worries. We’ll get to that soon.)

In this example, the AccountService class exists because of the convention to have multiple layers. For example, you might have the UI layer, service layer, and repository layer. There is no problem with having multiple layers.

The problem is when the sole purpose of the layer to delegate calls.

This makes the code harder to understand and debug. And if a code is hard to understand and debug, it won’t have all the necessary tests. So even though there is a test for this delegate method, the test by itself is not relevant enough to exist. As is, there is no logic that you can change. And the chances are that the method will stay like this forever.

The best code is no code at all.

Jeff Antwood

The conclusion is: don’t have layers for the sake of having layers. Instead, consider removing layers that don’t contain any logic. Remove unnecessary classes. There is more than enough code in this world that has to be maintained.

Now, what about the meaning of life?

This joke explains it best: I jumped off a cliff, searching for the meaning of life. And then it hit me…

Story Number 2 – How much of a complicated is too complicated?

May the Mock be with you.

Yoda, 900 years, Senior Developer at Microsoft

Life is complicated. Code is complicated. But we need to find ways to simplify it and get people to understand what we are trying to do.

Often, you will see a class in the code that takes too many constructor parameters.

public OrderController(IOrderRepository orderRepository,
                        IInvoiceRepository invoiceRepository,
                        ICustomerService customerService,
                        IPriceCalculator priceCalculator,
                        ILogger logger,
                        IEmailSender emailSender,
                        IReportGenerator reportGenerator,
                        ITaxCalculator taxCalculator)
{
}

The class that has 8 dependencies is probably violating the Single Responsibility Principle.

This becomes more obvious when you take a look at the setup part of your unit test. Most likely, every single test that checks the logic of the OrderController has dozen of lines of the setup code. And you need to repeat this setup for every unit test.

You could use Extract Method refactoring to put the setup into a separate method, and call this method from your test methods, but this just hides the problem.

The conclusion is: The class has too many responsibilities. Use Extract Class and Move Method refactorings to split it into several classes.

Story Number 3 – Living on the edge

Test. Unit Test.

James Bond, 32 years, QA Engineer at MI6

Living on the edge is something that many people don’t want to do.

Why?

For some, it is because of the risk that it may not pay off in the long run.

Others, however, enjoy living in a more risky environment. These people have more of a chance to succeed, but also lose much more if they fail. That is why it is important to find balance in life.

Like in life, it’s important to balance your test efforts. This means you should use not only unit tests, but also integration and UI (end-to-end) tests.

Let’s revisit the AccountService class from Story 1. The Save method now contains more logic.

public bool Save(Account account)
{
    try
    {
        bool isAccountValid = _acountValidator.IsAccountValid(account);
        if (!isAccountValid)
        {
            return false;
        }
        _accountRepository.SaveAccountToDatabase(account);
        return true;
    }
    catch (Exception ex)
    {
        _loger.LogError(ex, "Error while saving account to database");
        return false;
    }
}

As you can see, the method first checks whether the account is valid with the IAccountValidator. If the account is valid, then it uses IAccountRepository to save the account to the database. If anything goes wrong, the exception will be captured with the ILogger. The unit test that checks that the account was saved to the database looks like this.

[Fact]
public void Save_saves_account_to_database()
{
    var repositoryMock = new Mock<IAccountRepository>();
    var validatorStub = new Mock<IAccountValidator>();
    validatorStub
        .Setup(x => x.IsAccountValid(It.IsAny<Account>()))
        .Returns(true);

    var sut = new ThirdStory.AccountService(repositoryMock.Object,
        new Mock<ILogger>().Object,
        validatorStub.Object);

    sut.Save(new Account());

    repositoryMock.Verify(x => x.SaveAccountToDatabase(It.IsAny<Account>()), Times.Once);
}

The test uses validatorStub and repositoryMock to fake dependencies (you can learn about stubs and mocks in this article). It then verifies that the SaveAccountToDatabase method was called.

The test itself is fine, the only issue is with the test name.

You see, the unit test cannot really check that the account was created in the actual database. For that, you need an integration test. You need to set up a test database, call the Save method and then check that the new account is in the database.

The conclusion is: to get the most out of tests, you need to balance your test efforts. You need several layers of tests: unit, integration, and UI tests. In other words, you need a testing pyramid. But that’s an old conclusion. The one that Egyptians figured out 3000 years ago.

So, tests can talk, after all

After all this time? – Always.

Severus Snape, 38 years, Rockstar Wizard Developer at Uber

Tests can talk.
And you should listen to them.
Because they can tell beautiful stories.
But they can also help you to design a better code.