Статьи

Модульное тестирование контроллеров Spring MVC: конфигурация

Написание модульных тестов для контроллеров Spring MVC традиционно было простым и проблематичным. Хотя написать модульные тесты, которые вызывают методы контроллера, довольно просто, проблема в том, что эти модульные тесты недостаточно полны. Например, мы не можем тестировать сопоставления контроллера, валидацию и обработку исключений, просто вызывая протестированный метод контроллера.

Spring MVC Test решил эту проблему, дав нам возможность вызывать методы контроллера через DispatcherServlet . Это первая часть моего руководства, в которой описывается модульное тестирование контроллеров Spring MVC, и описывается, как мы можем настроить наши модульные тесты. Давайте начнем.

Получение необходимых зависимостей с Maven

Мы можем получить необходимые зависимости, объявив следующие тестовые зависимости в нашем файле pom.xml :

  • Юнит 4.11
  • Mockito Core 1.9.5
  • Весенний тест 3.2.3. РЕЛИЗ

Соответствующая часть нашего файла pom.xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.3.RELEASE</version>
    <scope>test</scope>
</dependency>

Давайте двигаться дальше и кратко рассмотрим наш пример приложения.

Анатомия нашего примера приложения

Пример приложения этого руководства предоставляет CRUD-операции для записей todo. Чтобы понять конфигурацию нашего тестового класса, мы должны иметь некоторые знания о тестируемом классе контроллера.

На данный момент нам нужно знать ответы на эти вопросы:

  • Какие у него зависимости?
  • Как это создается?

Мы можем получить ответы на эти вопросы, взглянув на исходный код класса TodoController . Соответствующая часть класса TodoController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
 
@Controller
public class TodoController {
 
    private final TodoService service;
 
    private final MessageSource messageSource;
 
    @Autowired
    public TodoController(MessageSource messageSource, TodoService service) {
        this.messageSource = messageSource;
        this.service = service;
    }
 
    //Other methods are omitted.
}

Как мы видим, наш класс контроллера имеет две зависимости: TodoService и MessageSource . Также мы видим, что наш класс контроллера использует инжекцию конструктора.

На данный момент это вся необходимая нам информация. Далее мы поговорим о конфигурации нашего приложения.

Настройка контекста приложения

Ведение отдельных конфигураций контекста приложения для нашего приложения и наших тестов обременительно. Кроме того, это может привести к проблемам, если мы что-то изменим в конфигурации контекста приложения нашего приложения, но забудем сделать то же самое для нашего тестового контекста.

Вот почему конфигурация контекста приложения-примера была разделена таким образом, что мы можем повторно использовать ее части в наших тестах.

Конфигурация контекста нашего приложения была разделена следующим образом:

  • Первый класс конфигурации приложения называется ExampleApplicationContext и является «основным» классом конфигурации нашего приложения.
  • Второй класс конфигурации отвечает за настройку веб-уровня нашего приложения. Имя этого класса — WebAppContext, и это класс конфигурации, который мы будем использовать в наших тестах.
  • Третий класс конфигурации называется PersistenceContext и содержит конфигурацию постоянства нашего приложения.

Примечание. В примере приложения также имеется конфигурация рабочего контекста приложения, в которой используются файлы конфигурации XML. Конфигурационные файлы XML, которые соответствуют классам конфигурации Java: exampleApplicationContext.xml , exampleApplicationContext-web.xml и exampleApplicationContext-persistence.xml .

Давайте посмотрим на конфигурацию контекста приложения нашего веб-слоя и выясним, как мы можем настроить наш тестовый контекст.

Настройка веб-слоя

Конфигурация контекста приложения веб-слоя имеет следующие обязанности:

  1. Это позволяет аннотации на основе Spring MVC.
  2. Он настраивает расположение статических ресурсов, таких как файлы CSS и файлы Javascript.
  3. Это гарантирует, что статические ресурсы обслуживаются сервлетом контейнера по умолчанию.
  4. Это гарантирует, что классы контроллера будут найдены во время сканирования компонента.
  5. Он настраивает компонент ExceptionResolver .
  6. Он настраивает компонент ViewResolver .

Давайте продолжим и посмотрим на класс конфигурации Java и файл конфигурации XML.

Конфигурация Java

Если мы используем конфигурацию Java, исходный код класса WebAppContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
 
import java.util.Properties;
 
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {
        "net.petrikainulainen.spring.testmvc.common.controller",
        "net.petrikainulainen.spring.testmvc.todo.controller"
})
public class WebAppContext extends WebMvcConfigurerAdapter {
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("/static/");
    }
 
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
 
    @Bean
    public SimpleMappingExceptionResolver 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;
    }
 
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
 
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
 
        return viewResolver;
    }
}

Конфигурация XML

Если мы используем конфигурацию XML, содержимое файла exampleApplicationContext-web.xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>
 
    <mvc:annotation-driven/>
 
    <mvc:resources mapping="/static/**" location="/static/"/>
    <mvc:default-servlet-handler/>
 
    <context:component-scan base-package="net.petrikainulainen.spring.testmvc.common.controller"/>
    <context:component-scan base-package="net.petrikainulainen.spring.testmvc.todo.controller"/>
 
    <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException">error/404</prop>
                <prop key="java.lang.Exception">error/error</prop>
                <prop key="java.lang.RuntimeException">error/error</prop>
            </props>
        </property>
        <property name="statusCodes">
            <props>
                <prop key="error/404">404</prop>
                <prop key="error/error">500</prop>
            </props>
        </property>
    </bean>
 
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    </bean>
</beans>

Настройка тестового контекста

Конфигурация нашего тестового контекста имеет две обязанности:

  1. Он настраивает компонент MessageSource, который используется нашим классом контроллера (сообщения обратной связи) и Spring MVC (сообщения об ошибках проверки). Причина, по которой нам нужно это сделать, заключается в том, что bean- компонент MessageSource сконфигурирован в «основном» классе (или файле) конфигурации конфигурации нашего приложения.
  2. Это создает макет TodoService, который вводится в наш класс контроллера.

Давайте выясним, как мы настраиваем наш тестовый контекст, используя класс конфигурации Java и файл конфигурации XML.

Конфигурация Java

Если мы настроим наш тестовый контекст с помощью конфигурации Java, исходный код класса TestContext будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.mockito.Mockito;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
 
@Configuration
public class TestContext {
 
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
 
        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
 
        return messageSource;
    }
 
    @Bean
    public TodoService todoService() {
        return Mockito.mock(TodoService.class);
    }
}

Конфигурация XML

Если мы настроим наш тестовый контекст с помощью конфигурации XML, содержимое файла testContext.xml будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
 
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n/messages"/>
        <property name="useCodeAsDefaultMessage" value="true"/>
    </bean>
 
    <bean id="todoService" name="todoService" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg value="net.petrikainulainen.spring.testmvc.todo.service.TodoService"/>
    </bean>
</beans>

Настройка тестового класса

Мы можем настроить наш тестовый класс, используя одну из следующих опций:

  1. Автономная конфигурация позволяет нам регистрировать один или несколько контроллеров (классы, помеченные аннотацией @Controller ) и программно конфигурировать инфраструктуру Spring MVC. Этот подход является приемлемым вариантом, если наша конфигурация Spring MVC проста и понятна.
  2. Конфигурация на основе WebApplicationContext позволяет нам конфигурировать инфраструктуру Spring MVC, используя полностью инициализированный WebApplicationContext. Этот подход лучше, если наша конфигурация Spring MVC настолько сложна, что использование автономной конфигурации не имеет никакого смысла.

Давайте продолжим и выясним, как мы можем настроить наш тестовый класс, используя обе опции конфигурации.

Использование автономной конфигурации

Мы можем настроить наш тестовый класс, выполнив следующие действия:

  1. Пометьте класс аннотацией @RunWith и убедитесь, что тест выполнен с использованием MockitoJUnitRunner .
  2. Добавьте поле MockMvc в тестовый класс.
  3. Добавьте поле TodoService в тестовый класс и аннотируйте поле аннотацией @Mock . Эта аннотация помечает поле как макет. Поле инициализируется MockitoJUnitRunner .
  4. Добавьте закрытый метод exceptionResolver () в класс. Этот метод создает новый объект SimpleMappingExceptionResolver , настраивает его и возвращает созданный объект.
  5. Добавьте приватный метод messageSource () в класс. Этот метод создает новый объект ResourceBundleMessageSource , настраивает его и возвращает созданный объект.
  6. Добавьте приватный метод validator () в класс. Этот метод создает новый объект LocalValidatorFactoryBean и возвращает созданный объект.
  7. Добавьте приватный метод viewResolver () в класс. Этот метод создает новый объект InternalResourceViewResolver , настраивает его и возвращает созданный объект.
  8. Добавьте метод setUp () в тестовый класс и аннотируйте метод аннотацией @Before . Это гарантирует, что метод вызывается перед каждым тестом. Этот метод создает новый объект MockMvc , вызывая метод standaloneSetup () класса MockMvcBuilders и программно настраивая инфраструктуру Spring MVC.

Исходный код нашего тестового класса выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
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.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
 
import java.util.Properties;
 
@RunWith(MockitoJUnitRunner.class)
public class StandaloneTodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Mock
    private TodoService todoServiceMock;
 
    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(messageSource(), todoServiceMock))
                .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 MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
 
        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
 
        return messageSource;
    }
 
    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;
    }
}

Использование автономной конфигурации имеет две проблемы:

  1. Наш тестовый класс выглядит как беспорядок, хотя наша конфигурация Spring MVC довольно проста. Естественно, мы могли бы это очистить, перенеся создание компонентов инфраструктуры Spring MVC в отдельный класс. Это оставлено в качестве упражнения для читателя.
  2. Мы должны продублировать конфигурацию компонентов инфраструктуры Spring MVC. Это означает, что если мы что-то изменим в конфигурации контекста приложения нашего приложения, мы должны помнить, что мы должны сделать то же самое в наших тестах.

Использование конфигурации на основе WebApplicationContext

Мы можем настроить наш тестовый класс, выполнив следующие действия:

  1. Пометьте тестовый класс аннотацией @RunWith и убедитесь, что тест выполняется с использованием SpringJUnit4ClassRunner .
  2. Пометьте класс аннотацией @ContextConfiguration и убедитесь, что используются правильные классы конфигурации (или файлы конфигурации XML). Если мы хотим использовать конфигурацию Java, мы должны установить классы конфигурации в качестве значения атрибута classes . С другой стороны, если мы предпочитаем конфигурацию XML, мы должны установить файлы конфигурации в качестве значения атрибута location .
  3. Аннотируйте класс с помощью аннотации @WebAppConfiguration . Эта аннотация гарантирует, что контекст приложения, который загружается для нашего теста, является WebApplicationContext .
  4. Добавьте поле MockMvc в тестовый класс.
  5. Добавьте поле TodoService в тестовый класс и добавьте аннотацию @Autowired .
  6. Добавьте поле WebApplicationContext в тестовый класс и аннотируйте поле аннотацией @Autowired .
  7. Добавьте метод setUp () в тестовый класс и аннотируйте метод аннотацией @Before. Это гарантирует, что метод вызывается перед каждым тестом. Этот метод имеет обязанности: он сбрасывает макет службы перед каждым тестом и создает новый объект MockMvc , вызывая метод webAppContextSetup () класса MockMvcBuilders .

Исходный код нашего тестового класса выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 = {TestContext.class, WebAppContext.class})
//@ContextConfiguration(locations = {"classpath:testContext.xml", "classpath:exampleApplicationContext-web.xml"})
@WebAppConfiguration
public class WebApplicationContextTodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    @Autowired
    private WebApplicationContext webApplicationContext;
 
    @Before
    public void setUp() {
        //We have to reset our mock between tests because the mock objects
        //are managed by the Spring container. If we would not reset them,
        //stubbing and verified behavior would "leak" from one test to another.
        Mockito.reset(todoServiceMock);
 
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }
}

Конфигурация нашего тестового класса выглядит намного чище, чем конфигурация, которая использует автономную конфигурацию. Однако недостатком является то, что наш тест использует полную инфраструктуру Spring MVC. Это может быть излишним, если наш тестовый класс действительно использует только несколько компонентов.

Резюме

Теперь мы настроили наш класс модульного тестирования, используя как автономную настройку, так и настройку на основе WebApplicationContext . Этот пост научил нас двум вещам:

  • Мы узнали, что важно разделить конфигурацию контекста приложения таким образом, чтобы мы могли повторно использовать ее части в наших тестах.
  • Мы узнали разницу между автономной конфигурацией и конфигурацией на основе WebApplicationContext .

Следующая часть этого руководства описывает, как мы можем написать модульные тесты для «обычных» контроллеров Spring MVC.

PS Пример приложения этого блога доступен на Github .