Core Java

Writing Clean Tests – It Starts from the Configuration

It is pretty hard to figure out a good definition for clean code because everyone of us has our own definition for the word clean. However, there is one definition which seems to be universal:

Clean code is easy to read.

This might come as a surprise to some of you, but I think that this definition applies to test code as well. It is in our best interests to make our tests as readable as possible because:
 

  • If our tests are easy to read, it is easy to understand how our code works.
  • If our tests are easy to read, it is easy to find the problem if a test fails (without using a debugger).

It isn’t hard to write clean tests, but it takes a lot of practice, and that is why so many developers are struggling with it.

I have struggled with this too, and that is why I decided to share my findings with you.

This is the first part of my tutorial which describes how we can write clean tests. This time we will learn how we can configure our test cases in a simple and clean way.

The Problem

Let’s assume that we have to write “unit tests” for Spring MVC controllers by using the Spring MVC Test framework. The first controller which we are going to test is called TodoController, but we have to write “unit tests” for the other controllers of our application as well.

As developers, we know that duplicate code is a bad thing. When we write code, we follow the Don’t repeat yourself (DRY) principle which states that:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

I suspect that this is one reason why developers often use inheritance in their test suite. They see inheritance as a cheap and easy way to reuse code and configuration. That is why they put all common code and configuration to the base class (or classes) of the actual test classes.

Let’s see how we can configure our “unit tests” by using the approach.

First, we have to create an abstract base class which configures the Spring MVC Test framework and ensures that its subclasses can provide additional configuration by implementing the setUpTest(MockMvc mockMvc) method.

The source code of the AbstractControllerTest class looks as follows:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public abstract class AbstractControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webAppContext;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
        setupTest(MockMvc mockMvc)
    }
   
    protected abstract void setUpTest(MockMvc mockMvc);
}

Second, we have to implement the actual test class which creates the required mocks and a new controller object. The source code of the TodoControllerTest class looks as follows:

import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;

public class TodoControllerTest extends AbstractControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private TodoService serviceMock;
   
    @Override
    protected void setUpTest(MockMvc mockMvc) {
        Mockito.reset(serviceMock);
        this.mockMvc = mockMvc;
    }

    //Add test methods here
}

This test class looks pretty clean but it has one major flaw:

If we want to find out how our test cases are configured, we have to read the source code of the TodoControllerTest and AbstractControllerTest classes.

This might seem like a minor issue but it means that we have to shift our attention from the test cases to the base class (or classes). This requires a mental context switch, and context switching is VERY expensive.

You might of course argue that the mental price of using inheritance in this case is pretty low because the configuration is pretty simple. That is true, but, it is good to remember that this isn’t always the case in real life applications.

The real cost of context switching depends from the depth of the test class hierarchy and the complexity of our configuration.

The Solution

We can improve the readability of our configuration by configuring all test cases in the test class. This means that we have to:

  • Add the required annotations (such as @RunWith) to the test class.
  • Add the setup and teardown methods to the test class.

If we modify our example test class by following these rules, its source code looks as follows:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class TodoControllerTest {

    private MockMvc mockMvc;
   
    @Autowired
    private TodoService serviceMock;

    @Autowired
    private WebApplicationContext webAppContext;

    @Before
    public void setUp() {
        Mockito.reset(serviceMock);
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
    }

    //Add test methods here
}

In my opinion, the new configuration of our test cases looks a lot simpler and cleaner than the old configuration which was divided into TodoControllerTest and AbstractControllerTest classes.

Unfortunately, nothing is free.

This is a Trade-Off

Every software design decision is a trade-off which has both pros and cons. This is not an exception to that rule.

Configuring our test cases in the test class has the following benefits:

  1. We can understand the configuration of our test cases without reading all superclasses of the test class. This saves a lot of time because we don’t have to shift our attention from one class to another. In other words, we don’t have to pay the price of context switching.
  2. It saves time when a test fails. If we would use inheritance because we want to avoid duplicate code or configuration, the odds are that our base classes would contain components which are relevant to some but not all test cases. In other words, we would have figure which components are relevant to the failed test case, and this might not be an easy task. When we configure our test cases in the test class, we know that every component is relevant to the failing test case.

On the other hands, the cons of this approach are:

  1. We have to write duplicate code. This takes longer than putting the required configuration to the base class (or classes).
  2. If any of the used libraries change in a way that forces us to modify the configuration of our tests, we have to make the required changes to every test class. This is obviously a lot slower than making these only to the base class (or classes).

If our only goal is to write our tests as fast as possible, it is clear that we should eliminate duplicate code and configuration.

However, that is not my only goal.

There are two reasons why I think that the benefits of this approach outweigh its drawbacks:

  1. Inheritance is not the right tool for reusing code or configuration.
  2. If a test case fails, we must find and solve the problem as soon as possible, and a clean configuration will help us to achieve that goal.

My stand in this matter is crystal clear. However, there is still one very important question left:

Will you make a different trade-off?

Petri Kainulainen

Petri is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Martin Vanek
Martin Vanek
10 years ago

I would still prefer first. It is not configuration but repetitive plumbing without any logic and it is only enforced by testing framework.

Your WebUnitTestContext is Configuration, but I believe that you gave up on that because you know you cannot put it into every test class.

It is true that inheritance is not greatest way to share code, but I can’t see problem here…

Petri Kainulainen
10 years ago
Reply to  Martin Vanek

“I would still prefer first. It is not configuration but repetitive plumbing without any logic and it is only enforced by testing framework.” That is fine. You must use the approach that makes sense to you. “Your WebUnitTestContext is Configuration, but I believe that you gave up on that because you know you cannot put it into every test class.” Yes. It is a trade-off. It simply isn’t practical to create a separate application context configuration file for each integration test (The test isn’t really a pure unit test). I am starting to think that maybe I should have used… Read more »

Back to top button