Writing Unit Tests for "Normal" Controllers - Configuration
After we have finished this lesson, we
- Know how we can select the required components.
- Understand how we can include additional components in the system under test.
This lesson assumes that:
What Components Do We Need?
When we use the standalone configuration, we can select the components that we want to include in the system under test by using the API of the StandaloneMockMvcBuilder class. This is both a blessing and a curse.
This gives us a lot of flexibility, but this flexibility is a double edged sword. It can cause two problems:
First, if we include a component in the system under test, the person who reads our test code assumes that the component is required by our test cases. If this is not the case, our configuration is misleading and makes our tests hard to read. To make matters worse, if the person who reads our test code is reading it because a test case failed, a misleading configuration can cost this person a lot of time.
Second, because the StandaloneMockMvcBuilder class allows us to configure pretty much every component that is provided by the Spring MVC framework, it can be quite compelling to take advantage of this opportunity. However, if decide to do so, we end up writing tests that aren’t really unit tests.
These tests are typically hard to write because we have to write too many assertions AND these tests are also hard to maintain because we have to sync the configuration of our application and the configuration that we use when we run our unit tests.
That is why I think that we should minimize the number of custom components that we include in the system under test. Also, we shouldn’t use the same configuration in every test class because most likely different controllers don’t require the same components.
Here is a list of components that I typically use when I am writing unit tests for “normal” Spring MVC controllers that show the result of an HTTP request by rendering a view:
- I configure the used
ExceptionResolverif my application has error views that are rendered when a controller method throws an exception. - I specify the used
LocaleResolverif aLocaleobject is injected into a method of the tested controller as a method parameter. - I specify the used
ViewResolverif I don’t want that my unit tests use theInternalViewResolverthat is used by the Spring MVC Test framework if no view resolver is configured.
Important Note:
- We don’t have to configure a custom
ViewResolverbecause we will write assertions only for the name the returned view. This means that we can use theInternalViewResolver. However, I will configure a customViewResolverhere because I want to show you how you can do it. - If you want to include other components in the system under test, feel free to do so. My list is quite short and I understand that you might have different requirements. Do not take this list literally! :)
As you can see, I don’t use many custom components because I want that my unit tests are simple, fast, easy to understand, and easy to write and maintain.
Next, we will find out how we can create and configure these components by using factory methods.
Creating the Required Components
We can create the required components by adding new static factory methods into the WebTestConfig class. To be more specific, we have to add the following methods into the WebTestConfig class:
- The
exceptionResolver()method creates a newSimpleMappingExceptionResolverobject that selects the rendered error view by using the type of the thrown exception. We can also configure the HTTP status code that is returned when an error view is rendered. - The
fixedLocaleResolver()method returns a newFixedLocaleResolverobject that always returnsLocale.ENGLISH. It is a good idea to use a fixed locale because it ensures that the environment in which our tests are run cannot cause false positives (aka test failures). - The
jspViewResolver()method creates a newViewResolverobject that uses JSP views. If our application uses some other view technology, we should replace this method with a method that creates aViewResolverobject that supports the used view technology. Of course, we should do this only if we really need to configure a customViewResolver. Remember that the odds are that we don’t really need it.
After we have added these factory methods into our object mother class, its source code looks as follows:
1import org.springframework.validation.Validator;
2import org.springframework.web.servlet.LocaleResolver;
3import org.springframework.web.servlet.ViewResolver;
4import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
5import org.springframework.web.servlet.i18n.FixedLocaleResolver;
6import org.springframework.web.servlet.view.InternalResourceViewResolver;
7import org.springframework.web.servlet.view.JstlView;
8
9import java.util.Locale;
10import java.util.Properties;
11
12public final class WebTestConfig {
13
14 private WebTestConfig() {}
15
16 public static SimpleMappingExceptionResolver exceptionResolver() {
17 SimpleMappingExceptionResolver exceptionResolver =
18 new SimpleMappingExceptionResolver();
19
20 Properties viewMappings = new Properties();
21
22 viewMappings.put(
23 "com.testwithspring.starter.spring.task.TaskNotFoundException",
24 "error/404"
25 );
26 viewMappings.put("java.lang.Exception", "error/error");
27 viewMappings.put("java.lang.RuntimeException", "error/error");
28
29 exceptionResolver.setExceptionMappings(viewMappings);
30
31 Properties statusCodes = new Properties();
32
33 statusCodes.put("error/404", "404");
34 statusCodes.put("error/error", "500");
35
36 exceptionResolver.setStatusCodes(statusCodes);
37
38 return exceptionResolver;
39 }
40
41 public static LocaleResolver fixedLocaleResolver() {
42 return new FixedLocaleResolver(Locale.ENGLISH);
43 }
44
45 public static ViewResolver jspViewResolver() {
46 InternalResourceViewResolver viewResolver =
47 new InternalResourceViewResolver();
48
49 viewResolver.setViewClass(JstlView.class);
50 viewResolver.setPrefix("/WEB-INF/jsp/");
51 viewResolver.setSuffix(".jsp");
52
53 return viewResolver;
54 }
55}
Additional Reading:
Let’s find out how we can include these components in the system under test.
Configuring the System Under Test
Before we can configure the system under test by using the standalone configuration, we have to identify the dependencies of the tested controller.
Let’s assume that we are writing unit tests for the TaskCrudController class whose source code looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.context.MessageSource;
3import org.springframework.stereotype.Controller;
4
5@Controller
6public class TaskCrudController {
7
8 private final TaskCrudService crudService;
9 private final MessageSource messageSource;
10
11 @Autowired
12 TaskCrudController(TaskCrudService crudService,
13 MessageSource messageSource) {
14 this.crudService = crudService;
15 this.messageSource = messageSource;
16 }
17
18 //The other methods are not relevant yet
19}
As we can see, this controller has two dependencies:
- The
TaskCrudServiceobject provides CRUD operations for tasks. - The
MessageSourceobject is used to fetch the feedback messages shown to the user.
After we have identified the dependencies of the tested controller, we have to create these dependencies in the setup method that is invoked before a test method is invoked. We can do this by following these steps:
- Add
TaskCrudServiceandStaticMessageSourcefields into our test class. - Add a setup method into our test class. We have to create the dependencies of the tested controller in this method.
- Create a new
TaskCrudServicemock and set the created mock object to theTaskCrudServicefield. - Create a new
StaticMessageSourceobject and set the created object to theStaticMessageSourcefield. We will use theStaticMessageSourceclass because it allows us to register message programmatically and I think that this is a lot cleaner than configuring the return value of a stubbed method.
After we have created the dependencies of the tested controller, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.context.support.StaticMessageSource;
6import org.springframework.test.web.servlet.MockMvc;
7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
8
9@RunWith(HierarchicalContextRunner.class)
10@Category(UnitTest.class)
11public class TaskCrudControllerTest {
12
13 private TaskCrudService crudService;
14 private StaticMessageSource messageSource;
15
16 @Before
17 public void configureSystemUnderTest() {
18 crudService = mock(TaskCrudService.class);
19 messageSource = new StaticMessageSource();
20 }
21}
Additional Reading:
Our next step is to create a new MockMvc object and configure the components that we want to include in the system under test. However, before we can do this, we have to figure out what components are required by our tests. Let’s go through the required components one by one:
- We don’t have to use a custom
ViewResolver, but we use it here because it’s useful to know how we can configure a customViewResolver. - We need an
ExceptionResolverbecause we want to ensure that the correct error view is rendered and that the correct HTTP status code is returned when a controller method throws an exception. - We need a
LocaleResolverbecause aLocaleobject is injected into a few controller methods as a method parameter.
After we have created a new MockMvc object and included these components in the system under test, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.context.support.StaticMessageSource;
6import org.springframework.test.web.servlet.MockMvc;
7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
8
9@RunWith(HierarchicalContextRunner.class)
10@Category(UnitTest.class)
11public class TaskCrudControllerTest {
12
13 private TaskCrudService crudService;
14 private StaticMessageSource messageSource;
15 private MockMvc mockMvc;
16
17 @Before
18 public void configureSystemUnderTest() {
19 crudService = mock(TaskCrudService.class);
20 messageSource = new StaticMessageSource();
21
22 TaskCrudController controller = new TaskCrudController(crudService,
23 messageSource
24 );
25 mockMvc = MockMvcBuilders.standaloneSetup(controller)
26 .setHandlerExceptionResolvers(exceptionResolver())
27 .setLocaleResolver(fixedLocaleResolver())
28 .setViewResolvers(jspViewResolver())
29 .build();
30 }
31}
Also, it is good to know that it is possible to register other components as well. That is why I recommend that you take a look at the Javadoc of the StandaloneMockMvcBuilder class.
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us two things:
- We should minimize the number of custom components and use only the components that are required by the tested controller.
- We can include custom components in the system under test by using the methods of the
StandaloneMockMvcBuilderclass.