Writing Clean Tests - It Starts From the Configuration

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?

20 comments… add one
  • Daniel May 4, 2014 @ 19:12

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

    • Petri May 4, 2014 @ 20:20

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

    • Nathaniel Jul 11, 2017 @ 11:50

      I'm a begginer developper (newly diplomed) and my first mission is to write it test for a complex IT to IT mechanism involving webservices and exotic data treatments.

      Well even if I read your article with a interest I think I'll stay with my abstract class. In some cases it seems to make more sense. Hence the mechanics I'm testing are subject to moving by form but not by nature I needed that abstract class as an anchor that saved me a lot of time and gave me tool to maintain the test integrity and save time through testing(change data testing or test environnement by replacing ressources files or constants).

      In the other hand I really loved your factory-esque example and will definitely use it in the future if able, feels perfect.

      • Petri Jul 11, 2017 @ 12:23

        Hi,

        I am proud of you because you don't change your mind just because you happened to find a blog post which claims that you should "never" use abstract base classes. If using an abstract base class makes sense, you should definitely use it. Sometimes it is (surprisingly) easy to forget that different situations require different tools, and there is nothing wrong with it.

        Anyway, I am happy that this blog post made you think and helped you to make an educated decision.

  • Kai Virkki May 7, 2014 @ 0:45

    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. ;)

    • Petri May 7, 2014 @ 9:18

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

  • li lin May 7, 2014 @ 10:35

    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

    • Petri May 7, 2014 @ 20:53

      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?

  • al0 Mar 25, 2015 @ 12:39

    As for me cons overweight pros by order of magnitude at least.

    • Petri Mar 25, 2015 @ 13:24

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

  • Kostadin Golev Apr 8, 2016 @ 13:37

    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 :)

    • Petri Apr 9, 2016 @ 11:41

      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!

  • lixinyi May 25, 2016 @ 12:16

    I'm a little bit confusing: did you use a junit test? how you implement the test(@Test) in each class? I mean, I think you use different configuration for several tests in this example, but I didn't find your implementation for these tests.

    • Petri May 27, 2016 @ 8:12

      Hi,

      All examples of this blog post use JUnit. The reason why these classes do not have any test methods is that writing tests is a quite broad topic and it's impossible to cover it in one blog post. That being said, if you want to know how to write unit and integration tests for Spring web applications, you should take a look at my Spring MVC Test tutorial.

      Also, if you have any additional questions don't hesitate to ask them.

  • Adam Mar 24, 2018 @ 0:30

    I used to also get mad when I saw people using inheritance to test this way. Inheritance is the tightest coupling two classes can have. If you have hundreds of sub-classes, you are one step away from having a class with thousands of lines.

    But now I feel DRY is more important. Here is an example why, a developer adds a new feature to 99 out of 100 scenarios, but forgets to add one. The developer then fixes the 99 broken tests, not realizing the one test they did not update only passes because it doesn't test the new feature. If instead, they just changed the base class, they would find the one test starts failing.

  • Todd Jan 17, 2019 @ 23:23

    I just got through writing around 50 tests for a Rest API project. An abstract class was the way to go without question. If I didn't use an abstract class/shared code, each unit test would've been a minimum of 50 lines of redundant code. As it stands now most tests are 5-7 lines of code.

    This article is awful advice. If you can make a case that inheritance shouldn't be used in test code then you should also make the same case for non-test code.

    Sure it takes time to understand the inheritance model for any code base. I'd rather take the time to understand the inheritance model versus taking the time to understand how 50 pieces of code work because they refused to use inheritance.

    • Petri Jan 18, 2019 @ 0:08

      Hi Todd,

      I just got through writing around 50 tests for a Rest API project. An abstract class was the way to go without question. If I didn’t use an abstract class/shared code, each unit test would’ve been a minimum of 50 lines of redundant code. As it stands now most tests are 5-7 lines of code.

      As always, it depends from the context. That being said, I don't use abstract base classes in my unit tests and none of them have 50 lines of "redundant" code that could be removed by using inheritance. Most of time the duplicate code is found from test classes which test Spring MVC controllers, and often they contain only about 10 lines of duplicate code (the setup method).

      This article is awful advice. If you can make a case that inheritance shouldn’t be used in test code then you should also make the same case for non-test code.

      In fact, I think that you should favor composition over inheritance (even in non-test code). However, I am in the business of learning new things. Thus, if you truly think that this blog post gives bad advice to its readers, I challenge you to write a blog post that provides better advice. I will include it in my Java Testing Weekly newsletter, and I promise to link it from this blog post as well.

      Sure it takes time to understand the inheritance model for any code base. I’d rather take the time to understand the inheritance model versus taking the time to understand how 50 pieces of code work because they refused to use inheritance.

      First, I am happy that you were able to solve your problems by using a technique that makes sense to you. That being said, are you sure that people don't use inheritance because they refuse to understand the inheritance model? Maybe they do understand the inheritance model and that's why they decide to not use it...

      • Todd Jan 18, 2019 @ 16:43

        I'm sorry if i came across a bit harsh in my original reply. I did appreciate your article and did in fact take away some positives from it. There we're things that I abstracted away (that maybe I shouldn't have) that would've made the tests more easy to understand.

        Also thanks for the link on composition over inheritance.

        Keep up the good work.

        • Petri Jan 18, 2019 @ 19:09

          Hi Todd,

          I’m sorry if i came across a bit harsh in my original reply.

          Don't worry about it. I don't think that your comment is harsh at all. Written communication is tricky because it's faceless, and that's why I always assume that people have the best intentions in mind when they write a comment on my blog. Also, because English not my mother tongue, I can appear harsh simply because I miss all the subtle nuances which are clear to a native speaker. Sorry about that.

          By the way, your comment made me realize that maybe I should update this tutorial since this was written a long time ago. Thank you for making me to see this.

          However, I was serious when I said that you should write a blog post about your solution. I would love to read it (and my original offer still stands). It's difficult to find people who write "technical" posts about automated testing, and that's why new bloggers are always welcome!

Leave a Reply