Get 30% discount from my upcoming Test With Spring Course.

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 classes.

If we want to write clean tests, we must configure our test classes 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 BIG mistake.

This blog post identifies the problems caused by the DRY principle and helps us to solve these problems.

Duplicate Code Is a Bad Thing

Let’s assume that we have to write “unit tests” for Spring MVC controllers by using the Spring MVC Test framework. We will start by writing unit tests for the TodoController class. However, we have to also write unit tests for the other controllers of our application.

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 this approach.

An Abstract Class to the Rescue

First, we have to create an abstract base class which configures the Spring MVC Test framework by using the standalone configuration and expects that its sub classes implement the getTestedController() method which returns the tested controller object.

The source code of the AbstractControllerTest class looks as follows:

import org.junit.Before;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public abstract class AbstractControllerTest {

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(getTestedController())
			.build();
    }
	
	protected MockMvc getMockMvc() {
		return mockMvc;
	}
	
	protected abstract Object getTestedController();
}

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

import static org.mockito.Mockito.mock;

public TodoControllerTest extends AbstractControllerTest {

    private TodoService service;

	@Override
	protected Object getTestedController() {
		service = mock(TodoService.class);
		return new TodoController(service);
	}
}

This test class looks pretty clean but it has two major flaws:

First, We cannot understand the configuration of our test class without reading 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 class to the base class (or classes). This requires a mental context switch, and context switching is VERY expensive.

You can argue that the mental price of using inheritance (in this case) is pretty low because the configuration is so simple. That is true, but it is good to remember that real-life applications often require more complex configuration.

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

Second, We cannot use different configuration for different test classes. For example, a typical scenario is that our web application has both normal controllers and REST controllers.

We could configure the created MockMvc object to support both controllers, but this is a bad idea because it makes our configuration more complex than it should be. This means that if a test case fails, it can be very hard to figure out if it failed because of a bug or because our configuration is not correct.

Also, I think that this violates the basic idea of unit testing which is to run our tests in an environment that contains only the code that is relevant for our tests. For example, if we are writing unit tests for a REST controller, we don’t need a ViewResolver or a SimpleMappingExceptionResolver. However, if we are writing unit tests for normal controller, we need these components, but we don’t need a MappingJackson2HttpMessageConverter or an ExceptionHandlerExceptionResolver.

How messy can it be? Well, I created an abstract base class that creates a MockMvc object which supports both normal controllers and REST controllers. Its source code looks as follows:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
import org.junit.Before;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

public abstract class AbstractControllerTest {

    private MockMvc mockMvc;

    @Before
    public void setUp() {
		StaticMessageSource messageSource = new StaticMessageSource();
		messageSource.setUseCodeAsDefaultMessage(true);	

        mockMvc = MockMvcBuilders.standaloneSetup(getTestedController())
			.setHandlerExceptionResolvers(exceptionResolver(), restErrorHandler(messageSource))
			.setMessageConverters(jacksonDateTimeConverter())
            .setValidator(validator())
            .setViewResolvers(viewResolver())
			.build();
    }
	
	protected MockMvc getMockMvc() {
		return mockMvc;
	}
	
	protected abstract Object getTestedController();
	
	private HandlerExceptionResolver exceptionResolver() {
		SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();

		Properties exceptionMappings = new Properties();	

		exceptionMappings.put(
			"net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", 
			"error/404"
		);
		exceptionMappings.put("java.lang.Exception", "error/error");
		exceptionMappings.put("java.lang.RuntimeException", "error/error");

		exceptionResolver.setExceptionMappings(exceptionMappings);

		Properties statusCodes = new Properties();	

		statusCodes.put("error/404", "404");
		statusCodes.put("error/error", "500");

		exceptionResolver.setStatusCodes(statusCodes);

		return exceptionResolver;
	}
	
	private MappingJackson2HttpMessageConverter jacksonDateTimeConverter() {
		ObjectMapper objectMapper = new ObjectMapper();

		objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		objectMapper.registerModule(new JSR310Module());

		MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
		converter.setObjectMapper(objectMapper);
		return converter;
	}
	
    private ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) {
		ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
			@Override
			protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod,
																			Exception exception) {
				Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method);
				}
				return super.getExceptionHandlerMethod(handlerMethod, exception);
			}
		};
		exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter()));
		exceptionResolver.afterPropertiesSet();
		return exceptionResolver;
	}
	
	private LocalValidatorFactoryBean validator() {
		return new LocalValidatorFactoryBean();
	}

	private ViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();

		viewResolver.setViewClass(JstlView.class);
		viewResolver.setPrefix("/WEB-INF/jsp/");
		viewResolver.setSuffix(".jsp");

		return viewResolver;
	}
}

IMO that looks pretty awful. However, if we want to keep following the DRY principle, we can try to clean this up by adding two new abstract classes into our test class hierarchy.

In DRY We Trust

If we want to clean up our mess, we have to create a class hierarchy that consists of the following classes:

  • The AbstractControllerTest class contains the common methods that are shared by the other abstract classes and the actual test classes.
  • The AbstractNormalControllerTest class extends the AbstractControllerTest class and provides support for writing unit tests for normal Spring MVC controllers.
  • The AbstractRESTControllerTest class extends the AbstractControllerTest class and provides support for writing unit tests for REST controllers.

The following figure illustrates the structure of our test class hierarchy:

controllertestclasshierarchy

Let’s take a closer look at each abstract class.

The AbstractControllerTest class contains the following methods:

  • The setUp() method is invoked before our test methods are invoked. This method invokes the buildSystemUnderTest() method and puts the returned MockMvc object into private mockMvc field.
  • The getMockMvc() method returns the configured MockMvc object. This method is used by actual test classes.
  • The validator() method returns a new LocalValidatorFactoryBean object. This method is invoked by other abstract classes when they configure the system under test.
  • The abstract buildSystemTest() must be implemented by other abstract classes. The implementation of this method must return a configured MockMvc object.
  • The abstract getTestedController() method returns an instance of the tested Spring MVC controller. This method must be implemented by actual test classes. It is invoked by our other abstract classes when they configure the system under test.

The source code of the AbstractControllerTest class looks as follows:

import org.junit.Before;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
 
public abstract class AbstractControllerTest {
 
    private MockMvc mockMvc;
 
    @Before
    public void setUp() {
        mockMvc = buildSystemUnderTest();
    }
     
    protected MockMvc getMockMvc() {
        return mockMvc;
    }

	protected LocalValidatorFactoryBean validator() {
		return new LocalValidatorFactoryBean();
	}

	protected abstract MockMvc buildSystemUnderTest();
     
    protected abstract Object getTestedController();
}

The AbstractNormalControllerTest class contains the following methods:

  • The buildSystemUnderTest() method creates a configured MockMvc object and returns the created object.
  • The exceptionResolver() method creates a new SimpleMappingExceptionResolver object that maps exceptions into view names. It also returns the created object.
  • The viewResolver() method creates a new InternalViewResolver object, configures its JSP support, and returns the created object.

The source code of the AbstractNormalControllerTest class looks as follows:

import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

public abstract class AbstractNormalControllerTest extends AbstractControllerTest {

    @Override
    protected MockMvc buildSystemUnderTest() {
        return MockMvcBuilders.standaloneSetup(getTestedController())
			.setHandlerExceptionResolvers(exceptionResolver())
            .setValidator(validator())
            .setViewResolvers(viewResolver())
			.build();
    }
	
	private HandlerExceptionResolver exceptionResolver() {
		SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();

		Properties exceptionMappings = new Properties();	

		exceptionMappings.put(
			"net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", 
			"error/404"
		);
		exceptionMappings.put("java.lang.Exception", "error/error");
		exceptionMappings.put("java.lang.RuntimeException", "error/error");

		exceptionResolver.setExceptionMappings(exceptionMappings);

		Properties statusCodes = new Properties();	

		statusCodes.put("error/404", "404");
		statusCodes.put("error/error", "500");

		exceptionResolver.setStatusCodes(statusCodes);

		return exceptionResolver;
	}

	private ViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();

		viewResolver.setViewClass(JstlView.class);
		viewResolver.setPrefix("/WEB-INF/jsp/");
		viewResolver.setSuffix(".jsp");

		return viewResolver;
	}
}

The AbstractRESTControllerTest class contains the following methods:

  • The buildSystemUnderTest() method creates a configured MockMvc object and returns the created object.
  • The jacksonDateTimeConverter() method creates a new ObjectMapper, and configures it to ignore null fields and support Java 8 date and time objects. It wraps the created object into a new MappingJackson2HttpMessageConverter object and returns the wrapper object.
  • The restErrorHandler() returns a new ExceptionHandlerExceptionResolver object that handles the exceptions thrown by the system under test.

The source code of the AbstractRESTControllerTest class looks as follows:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;

public abstract class AbstractRESTControllerTest extends AbstractControllerTest {

    @Override
    protected MockMvc buildSystemUnderTest() {
		StaticMessageSource messageSource = new StaticMessageSource();
		messageSource.setUseCodeAsDefaultMessage(true);	

        return MockMvcBuilders.standaloneSetup(getTestedController())
			.setHandlerExceptionResolvers(restErrorHandler(messageSource))
			.setMessageConverters(jacksonDateTimeConverter())
            .setValidator(validator())
			.build();
    }
	
	private MappingJackson2HttpMessageConverter jacksonDateTimeConverter() {
		ObjectMapper objectMapper = new ObjectMapper();

		objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		objectMapper.registerModule(new JSR310Module());

		MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
		converter.setObjectMapper(objectMapper);
		return converter;
	}
	
    private ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) {
		ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
			@Override
			protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod,
																			Exception exception) {
				Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method);
				}
				return super.getExceptionHandlerMethod(handlerMethod, exception);
			}
		};
		exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter()));
		exceptionResolver.afterPropertiesSet();
		return exceptionResolver;
	}
}

We have now created the required abstract classes. Our next step is to create the actual test classes. These classes must implement the getTestedController() method and extend the correct base class.

The source code of the NormalTodoControllerTest class looks as follows:

import static org.mockito.Mockito.mock;
 
public NormalTodoControllerTest extends AbstractNormalControllerTest {
 
    private TodoService service;
     
	@Override
    protected Object getTestedController() {
        service = mock(TodoService.class);
        return new TodoController(service);
    }
}

The source code of the RESTTodoControllerTest class looks as follows:

import static org.mockito.Mockito.mock;
 
public RESTTodoControllerTest extends AbstractRESTControllerTest {
 
    private TodoService service;
     
	@Override
    protected Object getTestedController() {
        service = mock(TodoService.class);
        return new TodoController(service);
    }
}

After a lot of hard work we were able to create a test class hierarchy that (IMO) doesn’t solve our problem. In fact, I argue that this class hierarchy makes our tests even harder to understand.

Even though the individual classes are “pretty clean”, the problem is that if we want to know how our tests are configured, we have to read the source code of the actual test class, the source code of the AbstractNormalControllerTest class or the source code of the AbstractRESTControllerTest class, and the source code of the AbstractControllerTest class. In other words, our code follows the DRY principle, but the price of the required mental context shift is a lot higher.

It is clear that we have to violate the DRY principle.

Breaking the Rules

If we follow the DRY principle and use inheritance for reusing code, we will end up with an impressive looking class hierarchy that is hard to understand.

We have to find another way to eliminate most of the duplicate code, and to configure the system under test in a way that is easy to understand and doesn’t require a mental context shift. I think that we can achieve these goals by following these rules:

  • We must configure the system under test in our test class. In other words, we must add the @Before method into the actual test class.
  • We must create the required mock objects in the actual test class.
  • If the system under test requires other objects (not mocks) that are used by more than one test class, we should create these objects by using factory methods or builders.
  • If the system under test requires other objects (not mocks) that are used by only one test class, we should create these objects in the test class.

Let’s rewrite our tests by following these rules.

This blog post assumes that we are writing unit tests for a real-life web application. In other words, we have to write tests for many “normal” controllers and REST controllers.

First, we have to create the factory methods which creates the objects required to configure the system under test. We can do this by following these steps:

  1. Create a WebTestConfig class and ensure that it cannot be instantiated.
  2. Add the following static factory methods into the WebTestConfig class:
    1. The exceptionResolver() method creates a new SimpleMappingExceptionResolver object that maps exceptions into view names. It also returns the created object.
    2. The jacksonDateTimeConverter() method creates a new ObjectMapper, and configures it to ignore null fields and support Java 8 date and time objects. It wraps the created object into a new MappingJackson2HttpMessageConverter object and returns the wrapper object.
    3. The messageSource() method creates a new StaticMessageSource object, configures it to use the message code as a default message, and returns the created object.
    4. The restErrorHandler() returns a new ExceptionHandlerExceptionResolver object that handles the exceptions thrown by the system under test.
    5. The validator() method returns a new LocalValidatorFactoryBean object.
    6. The viewResolver() method creates a new InternalViewResolver object, configures its JSP support, and returns the created object.

The source code of the WebTestConfig class looks as follows:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
import org.springframework.context.MessageSource;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

public final class WebTestConfig {

	private WebTestConfig() {}

	public static HandlerExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
 
        Properties exceptionMappings = new Properties();    
 
        exceptionMappings.put(
            "net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", 
            "error/404"
        );
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");
 
        exceptionResolver.setExceptionMappings(exceptionMappings);
 
        Properties statusCodes = new Properties();  
 
        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");
 
        exceptionResolver.setStatusCodes(statusCodes);
 
        return exceptionResolver;
    }
	
	public static MappingJackson2HttpMessageConverter jacksonDateTimeConverter() {
		ObjectMapper objectMapper = new ObjectMapper();
 
		objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		objectMapper.registerModule(new JSR310Module());
 
		MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();	
		converter.setObjectMapper(objectMapper);
		return converter;
	}
	
	public static MessageSource messageSource() {
		StaticMessageSource messageSource = new StaticMessageSource();
		messageSource.setUseCodeAsDefaultMessage(true);
		return messageSource;
	}
	
	public static ExceptionHandlerExceptionResolver restErrorHandler(MessageSource messageSource) {
		ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
			@Override
			protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod,
																			  Exception exception) {
				Method method = new ExceptionHandlerMethodResolver(RestErrorHandler.class).resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(new RestErrorHandler(messageSource), method);
				}
				return super.getExceptionHandlerMethod(handlerMethod, exception);
			}
		};
		exceptionResolver.setMessageConverters(Arrays.asList(jacksonDateTimeConverter()));
		exceptionResolver.afterPropertiesSet();
		return exceptionResolver;
	}
	
	public static LocalValidatorFactoryBean validator() {
		return new LocalValidatorFactoryBean();
	}
	
    public static ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
 
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
 
        return viewResolver;
    }
}

After we have created these factory methods, we have to rewrite our test classes. Every test class has two responsibilities:

  • It creates the required mock object.
  • It configures the system under test and creates a new MockMvc object that can be used for writing unit tests for controller methods.

After we have made these changes to the NormalTodoControllerTest class, its source code looks as follows:

import org.junit.Before;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.mock;
 
public class NormalTodoControllerTest {

	private MockMvc mockMvc;
	private TodoService service;

	@Before
	public void configureSystemUnderTest()
		service = mock(TodoService.class);

		mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(service))
			.setHandlerExceptionResolvers(WebTestConfig.exceptionResolver())
			.setValidator(WebTestConfig.validator())
			.setViewResolvers(WebTestConfig.viewResolver())
			.build();
	}
}

After we have rewritten the RESTTodoControllerTest class, its source code looks as follows:

import org.junit.Before;
import org.springframework.context.MessageSource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.mock;
 
public class RESTTodoControllerTest {

	private MockMvc mockMvc;
	private TodoService service;

	@Before
	public void configureSystemUnderTest()
		MessageSource messageSource = WebTestConfig.messageSource();
		service = mock(TodoService.class);
 
		mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(service))
			.setHandlerExceptionResolvers(WebTestConfig.restErrorHandler(messageSource))
			.setMessageConverters(WebTestConfig.jacksonDateTimeConverter())
			.setValidator(WebTestConfig.validator())
			.build();
	}
}

Let’s evaluate the pros and cons of this solution.

This Is a Tradeoff

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

If we configure our tests by following the rules described in the previous section, we can enjoy from these benefits:

  • We can get a general idea of our configuration by reading the method that configures the system under test. If we want to get more information about the configuration of a specific component, we can simply read the factory method that creates and configures it. In other words, our approach minimizes the cost of context shifting.
  • We can configure our test classes by using only the components which are relevant for every test method. This makes the configuration easier to understand and helps us to save time when a test case fails.

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

  • We have to write duplicate code. This takes a bit longer than putting the required configuration to the base class (or classes).
  • If we need to make changes to our configuration, we might have to make these changes to every test class.

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 three reasons why I think that the benefits of this approach outweigh its drawbacks:

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 how to create clean configuration for your unit, integration, and end-to-end tests, which ensure that your Spring web application is working as expected, take a look at my upcoming Test With Spring Course.

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 →

11 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
  • As for me cons overweight pros by order of magnitude at least.

    Reply
    • That is fine. You should always use the method that makes sense to you.

      Reply
  • I see the inheritance pattern you describe used a lot.

    Idea is almost always the same – make it faster to write code. Which IMHO is the wrong target for test code. All code should be easy to read, but it is a bit more important for tests. When things go wrong and fail, you want code that is easy to understand, right?

    Overall when team is small issue is not that big, but I was part of a big organisation where they actually created a “test framework” for their integration tests using inheritance. If you think 1 level of abstraction is bad, wait until you see 3-4 used in most of the tests…

    One additional thing I see here in your test code – since Spring 4.2 you can set ControllerAdvices on the mockMvc when using standaloneSetup:

    mockMvc = MockMvcBuilders.standaloneSetup(pushController)
    .setControllerAdvice(new YourControllerExceptionHandlerHere())
    .build();

    No need to write your own and duplicate code between “test” and “production” anymore :)

    Reply
    • Hi,

      Idea is almost always the same – make it faster to write code. Which IMHO is the wrong target for test code. All code should be easy to read, but it is a bit more important for tests. When things go wrong and fail, you want code that is easy to understand, right?

      I agree. If I have to choose between tests that are easy to read and tests that are fast to write, I choose tests that are easy read every time. However, this doesn’t mean that you shouldn’t make your tests easy to write. It just means that inheritance is not the right tool for achieving this goal.

      Like you said, this probably doesn’t matter in small projects. However, if you are working in a bigger project, this becomes a problem because if you use inheritance, you force your every test class to use the functions provided by your base classes (whether they need these functions or not). I guess that this is one reason why some people have to use the @DirtiesContext annotation that makes their test suite super slow because they cannot use the application context caching that is provided by Spring Test.

      One additional thing I see here in your test code – since Spring 4.2 you can set ControllerAdvices on the mockMvc when using standaloneSetup:

      Thanks for the tip. This is awesome!

      Reply

Leave a Comment