Do want to get a better understanding of Spring web application architecture? If so, get started right now!

Writing Clean Tests – It Starts from the Configuration

Messy network cables

The first thing that we have to do when we start writing either unit or integration tests is to configure our test cases.

If we want to write clean tests, we must configure our test cases in a clean and simple way. This seems obvious, right?

Sadly, some developers choose to ignore this approach in favor of the don’t repeat yourself (DRY) principle.

This is a mistake.

This blog posts identifies the problems of the DRY principle and describes a better way of configuring our test cases.

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?

If you want to learn more about writing clean tests, read all parts of my Writing Clean Tests tutorial.

About the Author

Petri Kainulainen 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.

About Petri Kainulainen →

6 comments… add one

  • >>we know that every component is relevant to the failing test case

    I don’t think this is true in a real life project. The configuration is too big to write it for every test new. Most programmers just copy it from a other unit test and change something. After some time there are many different version from the configuration and nobody knows why there are different.

    Reply
    • >>we know that every component is relevant to the failing test case

      I don’t think this is true in a real life project. The configuration is too big to write it for every test new.

      There is some truth in this. I configure my test cases in the following way:

      • If I write integration tests (the “unit test” used as an example is actually an integration test), I configure the test cases in the test class BUT I don’t create separate application context configuration files for each test class. Typically I follow these rules:
        • If I write end-to-end tests, I use the same application context configuration which is used when the application is deployed to the production environment.
        • If I write “unit tests” for Spring MVC controllers, I create a separate application context configuration class which configures test specific components, and include the application context configuration of the web layer to that class.
        • If I write integration tests for my repositories, I use the application context configuration class which configures the repository layer of my application.
      • If I write “clean” unit tests, the configuration isn’t usually very big and I put it always to each test class. This way I can ensure that I configure only the required components. A good example of a pure unit test is a test which tests a single service method.

      In other words, if I write integration tests, the configuration of my test cases might contain non-relevant components as well. The reality is that it isn’t very practical to create a new application context configuration for each test class.

      On the other hand, if I write pure unit tests, the configuration of my test cases is so small that it is practical to put it to the test class. If the configuration is any bigger than that, it indicates that the tested code is doing too much and it should be refactored / rewritten.

      Most programmers just copy it from a other unit test and change something. After some time there are many different version from the configuration and nobody knows why there are different.

      Copy and paste programming happens only if you allow it. We use a process where each commit must be reviewed before it can be added to the “main” branch of our project. At the moment we use a tool called Gerrit for this purpose. This is great way to share information to other team members and ensure that shitty code doesn’t end up to our “main” branch.

      Reply
  • I guess TodoControllerTest should extends AbstractControllerTest in your example. Now it doesn’t in the example code. Also, adding @Override annotation to the implementation of the abstract method “setUpTest” would make it more apparent to understand that we’re overriding a method from the superclass. ;)

    Reply
    • Good points! I will update the sample code. Thanks for pointing these mistakes out!

      Reply
  • I got some errors when i ovrride the simplejparepository to expand my customized method. I just difined the method : public T saveWithoutFlush(T entity);
    and then the errors like these:
    Caused by: org.springframework.data.mapping.PropertyReferenceException: No property save found for type User!

    Update: I removed the unnecessary information from the stacktrace. – Petri

    Reply
    • The problem is that the User entity doesn’t have a property called save. It is kind of hard to say what causes this without seeing the source code. Can you add the source code of your repository class to Pastebin and leave a new comment which contains the link to the source code of your repository?

      Reply

Leave a Comment