Статьи

Модульное тестирование контроллеров Spring MVC: «нормальные» контроллеры

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

Очевидный следующий вопрос:

Что такое нормальный контроллер?

Ну, обычный контроллер (в контексте этого поста в блоге) — это контроллер, который либо отображает представление, либо обрабатывает представления формы.

Давайте начнем.

Примечание . Я рекомендую вам прочитать первую часть этого руководства перед прочтением этого поста в блоге (если вы его уже прочитали, вы можете продолжить чтение)

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

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

  • Jackson 2.2.1 (модули ядра и базы данных). Мы используем Джексона для преобразования объектов в объекты String, закодированные в URL.
  • Hamcrest 1.3. Мы используем совпадения Hamcrest, когда пишем утверждения для ответов.
  • JUnit 4.11 (исключить зависимость от ядра hamcrest).
  • Мокито 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
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.2.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.2.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</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>

Давайте продолжим и выясним, как мы можем написать модульные тесты для контроллеров Spring MVC, используя среду тестирования Spring MVC.

Написание модульных тестов для методов контроллера

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

  1. Отправляем запрос на проверенный метод контроллера.
  2. Мы подтверждаем, что получили ожидаемый ответ.

Среда Spring MVC Test имеет несколько «базовых» классов, которые мы можем использовать для реализации этих шагов в наших тестах. Эти классы описаны ниже:

  • Мы можем строить наши запросы, используя статические методы класса MockMvcRequestBuilders . Или, если быть более точным, мы можем создать конструкторы запросов, которые затем передаются в качестве параметра метода методу, который выполняет фактический запрос.
  • Класс MockMvc является основной отправной точкой наших тестов. Мы можем выполнять запросы, вызывая его метод execute (RequestBuilder requestBuilder) .
  • Мы можем написать утверждения для полученного ответа, используя статические методы класса MockMvcResultMathers .

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

  • Первый метод контроллера отображает страницу, которая показывает список записей todo.
  • Второй метод контроллера отображает страницу, которая показывает информацию об одной записи todo.
  • Третий метод контроллера обрабатывает представления формы, которая используется для добавления новых записей задач в базу данных.

Визуализация страницы списка записей Todo

Давайте начнем с рассмотрения реализации метода контроллера, который используется для визуализации страницы списка записей todo.

Ожидаемое поведение

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

  1. Он получает записи todo, вызывая метод findAll () интерфейса TodoService . Этот метод возвращает список объектов Todo .
  2. Добавляет полученный список в модель.
  3. Возвращает имя визуализированного представления.

Соответствующая часть класса TodoController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
 
@Controller
public class TodoController {
 
    private final TodoService service;
    
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String findAll(Model model) {
        List<Todo> models = service.findAll();
        model.addAttribute("todos", models);
        return "todo/list";
    }
}

Теперь мы готовы написать модульный тест для этого метода. Посмотрим, как мы можем это сделать.

Тест: записи Todo найдены

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

  1. Создайте тестовые данные, которые возвращаются при вызове нашего сервисного метода. Мы используем концепцию, называемую построителем тестовых данных, когда мы создаем тестовые данные для нашего теста.
  2. Сконфигурируйте используемый mock-объект для возврата созданных тестовых данных при вызове его метода findAll () .
  3. Выполните запрос GET для URL ‘/’.
  4. Убедитесь, что возвращается код состояния HTTP 200.
  5. Убедитесь, что имя возвращаемого представления — «todo / list».
  6. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/todo/list.jsp.
  7. Убедитесь, что атрибут модели с именем todos содержит два элемента.
  8. Убедитесь, что атрибут модели с именем todos содержит правильные элементы.
  9. Убедитесь, что метод findAll () нашего фиктивного объекта был вызван только один раз.
  10. Убедитесь, что другие методы фиктивного объекта не были вызваны во время теста.

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

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
import org.junit.Before;
import org.junit.Test;
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;
 
import java.util.Arrays;
 
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    //Add WebApplicationContext field here
 
    //The setUp() method is omitted.
 
    @Test
    public void findAll_ShouldAddTodoEntriesToModelAndRenderTodoListView() throws Exception {
        Todo first = new TodoBuilder()
                .id(1L)
                .description("Lorem ipsum")
                .title("Foo")
                .build();
 
        Todo second = new TodoBuilder()
                .id(2L)
                .description("Lorem ipsum")
                .title("Bar")
                .build();
 
        when(todoServiceMock.findAll()).thenReturn(Arrays.asList(first, second));
 
        mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(view().name("todo/list"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/list.jsp"))
                .andExpect(model().attribute("todos", hasSize(2)))
                .andExpect(model().attribute("todos", hasItem(
                        allOf(
                                hasProperty("id", is(1L)),
                                hasProperty("description", is("Lorem ipsum")),
                                hasProperty("title", is("Foo"))
                        )
                )))
                .andExpect(model().attribute("todos", hasItem(
                        allOf(
                                hasProperty("id", is(2L)),
                                hasProperty("description", is("Lorem ipsum")),
                                hasProperty("title", is("Bar"))
                        )
                )));
 
        verify(todoServiceMock, times(1)).findAll();
        verifyNoMoreInteractions(todoServiceMock);
    }
}

Визуализация страницы входа Todo View

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

Ожидаемое поведение

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

  1. Он получает запрошенную запись todo путем вызова метода findById () интерфейса TodoService и передает идентификатор запрошенной записи todo в качестве параметра метода. Этот метод возвращает найденную запись todo. Если запись todo не найдена, этот метод генерирует исключение TodoNotEntryNotFoundException .
  2. Добавляет найденную запись todo в модель.
  3. Возвращает имя визуализированного представления.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
 
@Controller
public class TodoController {
 
    private final TodoService service;
 
    @RequestMapping(value = "/todo/{id}", method = RequestMethod.GET)
    public String findById(@PathVariable("id") Long id, Model model) throws TodoNotFoundException {
        Todo found = service.findById(id);
        model.addAttribute("todo", found);
        return "todo/view";
    }
}

Наш следующий вопрос:

Что происходит, когда выбрасывается исключение TodoEntryNotFoundException?

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@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;
}

Как мы видим, если выдается исключение TodoEntryNotFoundException , наше приложение отображает представление «error / 404» и возвращает код состояния HTTP 404.

Понятно, что мы должны написать два теста для этого метода контроллера:

  1. Мы должны написать тест, который гарантирует, что наше приложение работает правильно, когда запись todo не найдена.
  2. Мы должны написать тест, который проверяет, что наше приложение работает правильно, когда найдена запись todo.

Давайте посмотрим, как мы можем написать эти тесты.

Тест 1: запись Todo не найдена

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

  1. Сконфигурируйте фиктивный объект для генерирования исключения TodoNotFoundException, когда вызывается его метод findById () и идентификатор запрошенной записи todo равен 1L.
  2. Выполните GET- запрос к url ​​’/ todo / 1 ′.
  3. Убедитесь, что возвращается код состояния HTTP 404.
  4. Убедитесь, что имя возвращаемого представления — «ошибка / 404».
  5. Убедитесь, что запрос переадресован на URL ‘/WEB-INF/jsp/error/404.jsp’.
  6. Убедитесь, что метод findById () интерфейса TodoService вызывается только один раз с правильным параметром метода (1L).
  7. Убедитесь, что никакие другие методы фиктивного объекта не были вызваны во время этого теста.

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

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
import org.junit.Before;
import org.junit.Test;
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;
 
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    //Add WebApplicationContext field here
 
    //The setUp() method is omitted.
 
    @Test
    public void findById_TodoEntryNotFound_ShouldRender404View() throws Exception {
        when(todoServiceMock.findById(1L)).thenThrow(new TodoNotFoundException(""));
 
        mockMvc.perform(get("/todo/{id}", 1L))
                .andExpect(status().isNotFound())
                .andExpect(view().name("error/404"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/error/404.jsp"));
 
        verify(todoServiceMock, times(1)).findById(1L);
        verifyZeroInteractions(todoServiceMock);
    }
}

Тест 2: запись Todo найдена

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

  1. Создайте объект Todo, который возвращается, когда вызывается наш сервисный метод. Опять же, мы создаем возвращенный объект Todo с помощью нашего построителя тестовых данных.
  2. Сконфигурируйте наш фиктивный объект так, чтобы он возвращал созданный объект Todo, когда вызывается его метод findById () с использованием параметра метода 1L.
  3. Выполните GET- запрос к url ​​’/ todo / 1 ′.
  4. Убедитесь, что код состояния HTTP 200 возвращается.
  5. Убедитесь, что имя возвращаемого представления — «todo / view».
  6. Убедитесь, что запрос перенаправлен на URL /WEB-INF/jsp/todo/view.jsp.
  7. Убедитесь, что идентификатор объекта модели с именем todo равен 1L.
  8. Убедитесь, что описание модельного объекта с именем todo — «Lorem ipsum».
  9. Убедитесь, что заголовок объекта модели с именем todo — «Foo».
  10. Убедитесь, что метод findById () нашего фиктивного объекта вызывается только один раз с правильным параметром метода (1L).
  11. Убедитесь, что и другие методы фиктивного объекта не были вызваны во время нашего теста.

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

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
import org.junit.Before;
import org.junit.Test;
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;
 
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    //Add WebApplicationContext field here
 
    //The setUp() method is omitted.
 
    @Test
    public void findById_TodoEntryFound_ShouldAddTodoEntryToModelAndRenderViewTodoEntryView() throws Exception {
        Todo found = new TodoBuilder()
                .id(1L)
                .description("Lorem ipsum")
                .title("Foo")
                .build();
 
        when(todoServiceMock.findById(1L)).thenReturn(found);
 
        mockMvc.perform(get("/todo/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(view().name("todo/view"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/view.jsp"))
                .andExpect(model().attribute("todo", hasProperty("id", is(1L))))
                .andExpect(model().attribute("todo", hasProperty("description", is("Lorem ipsum"))))
                .andExpect(model().attribute("todo", hasProperty("title", is("Foo"))));
 
        verify(todoServiceMock, times(1)).findById(1L);
        verifyNoMoreInteractions(todoServiceMock);
    }
}

Работа с формой Подача заявки на добавление Todo

Опять же, мы сначала посмотрим на ожидаемое поведение нашего метода контроллера, прежде чем напишем для него модульные тесты.

Ожидаемое поведение

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

  1. Он проверяет, что объект BindingResult, указанный в качестве параметра метода, не имеет ошибок. Если ошибки найдены, он возвращает имя представления формы.
  2. Он добавляет новую запись Todo, вызывая метод add () интерфейса TodoService и передает объект формы в качестве параметра метода. Этот метод создает новую запись todo и возвращает ее.
  3. Он создает сообщение обратной связи о добавленной записи todo и добавляет сообщение в объект RedirectAttributes, указанный в качестве параметра метода.
  4. Он добавляет идентификатор добавленной записи todo в объект RedirectAttributes .
  5. Возвращает имя представления перенаправления, которое перенаправляет запрос на страницу входа todo view.

Соответствующая часть класса TodoController выглядит следующим образом:

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
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
import javax.validation.Valid;
import java.util.Locale;
 
@Controller
@SessionAttributes("todo")
public class TodoController {
 
    private final TodoService service;
 
    private final MessageSource messageSource;
 
    @RequestMapping(value = "/todo/add", method = RequestMethod.POST)
    public String add(@Valid @ModelAttribute("todo") TodoDTO dto, BindingResult result, RedirectAttributes attributes) {
        if (result.hasErrors()) {
            return "todo/add";
        }
 
        Todo added = service.add(dto);
 
        addFeedbackMessage(attributes, "feedback.message.todo.added", added.getTitle());
        attributes.addAttribute("id", added.getId());
 
        return createRedirectViewPath("todo/view");
    }
 
    private void addFeedbackMessage(RedirectAttributes attributes, String messageCode, Object... messageParameters) {
        String localizedFeedbackMessage = getMessage(messageCode, messageParameters);
        attributes.addFlashAttribute("feedbackMessage", localizedFeedbackMessage);
    }
 
    private String getMessage(String messageCode, Object... messageParameters) {
        Locale current = LocaleContextHolder.getLocale();
        return messageSource.getMessage(messageCode, messageParameters, current);
    }
 
    private String createRedirectViewPath(String requestMapping) {
        StringBuilder redirectViewPath = new StringBuilder();
        redirectViewPath.append("redirect:");
        redirectViewPath.append(requestMapping);
        return redirectViewPath.toString();
    }
}

Как мы видим, метод контроллера использует объект TodoDTO в качестве объекта формы. Класс TodoDTO — это простой класс DTO, исходный код которого выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
 
public class TodoDTO {
 
    private Long id;
 
    @Length(max = 500)
    private String description;
 
    @NotEmpty
    @Length(max = 100)
    private String title;
 
    //Constructor and other methods are omitted.
}

Класс TodoDTO объявляет некоторые ограничения проверки, которые описаны ниже:

  • Название записи todo не может быть пустым.
  • Максимальная длина описания составляет 500 символов.
  • Максимальная длина заголовка составляет 100 символов.

Если мы подумаем о тестах, которые мы должны написать для этого метода контроллера, то ясно, что мы должны убедиться, что

  1. Метод контроллера является рабочим свойством при сбое проверки.
  2. Метод контроллера является рабочим свойством, когда запись todo добавляется в базу данных.

Давайте выясним, как мы можем написать эти тесты.

Тест 1: проверка не проходит

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

  1. Создайте заголовок, который имеет 101 символ.
  2. Создайте описание, которое имеет 501 символов.
  3. Создайте новый объект TodoDTO с помощью нашего построителя тестовых данных. Установите заголовок и описание объекта.
  4. Выполните POST- запрос к URL ‘todo / add’. Установите тип содержимого запроса «application / x-www-form-urlencoded». Убедитесь, что содержимое нашего объекта формы отправлено в теле запроса. Установите объект формы в сеанс.
  5. Убедитесь, что код состояния HTTP 200 возвращается.
  6. Убедитесь, что имя возвращаемого представления — «todo / add».
  7. Убедитесь, что запрос пересылается по URL-адресу ‘/WEB-INF/jsp/todo/add.jsp’.
  8. Убедитесь, что у нашего атрибута модели есть ошибки полей в полях заголовка и описания .
  9. Убедитесь, что идентификатор нашего атрибута модели равен нулю.
  10. Убедитесь, что описание нашего атрибута модели является правильным.
  11. Убедитесь, что заголовок нашего атрибута модели правильный.
  12. Убедитесь, что методы нашего фиктивного объекта не были вызваны во время теста.

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

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
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
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;
 
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    //Add WebApplicationContext field here
 
    //The setUp() method is omitted.
 
    @Test
    public void add_DescriptionAndTitleAreTooLong_ShouldRenderFormViewAndReturnValidationErrorsForTitleAndDescription() throws Exception {
        String title = TestUtil.createStringWithLength(101);
        String description = TestUtil.createStringWithLength(501);
 
        TodoDTO formObject =  new TodoDTOBuilder()
                .description(description)
                .title(title)
                .build();
 
        mockMvc.perform(post("/todo/add")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(formObject))
                .sessionAttr("todo", formObject)
        )
                .andExpect(status().isOk())
                .andExpect(view().name("todo/add"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp"))
                .andExpect(model().attributeHasFieldErrors("todo", "title"))
                .andExpect(model().attributeHasFieldErrors("todo", "description"))
                .andExpect(model().attribute("todo", hasProperty("id", nullValue())))
                .andExpect(model().attribute("todo", hasProperty("description", is(description))))
                .andExpect(model().attribute("todo", hasProperty("title", is(title))));
 
        verifyZeroInteractions(todoServiceMock);
    }
}

Наш тестовый пример вызывает некоторые статические методы класса TestUtil . Эти методы описаны ниже:

  • Метод createStringWithLength (int length) создает новый объект String с заданной длиной и возвращает созданный объект.
  • Метод convertObjectToFormUrlEncodedBytes (Object object) преобразует объект в объект String, закодированный в форме URL, и возвращает содержимое этого объекта String в виде байтового массива.

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

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
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
 
public class TestUtil {
 
    public static byte[] convertObjectToFormUrlEncodedBytes(Object object) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
 
        Map<String, Object> propertyValues = mapper.convertValue(object, Map.class);
 
        Set<String> propertyNames = propertyValues.keySet();
        Iterator<String> nameIter = propertyNames.iterator();
 
        StringBuilder formUrlEncoded = new StringBuilder();
 
        for (int index=0; index < propertyNames.size(); index++) {
            String currentKey = nameIter.next();
            Object currentValue = propertyValues.get(currentKey);
 
            formUrlEncoded.append(currentKey);
            formUrlEncoded.append("=");
            formUrlEncoded.append(currentValue);
 
            if (nameIter.hasNext()) {
                formUrlEncoded.append("&");
            }
        }
 
        return formUrlEncoded.toString().getBytes();
    }
 
    public static String createStringWithLength(int length) {
        StringBuilder builder = new StringBuilder();
 
        for (int index = 0; index < length; index++) {
            builder.append("a");
        }
 
        return builder.toString();
    }
}

Тест 2: запись Todo добавлена ​​в базу данных

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

  1. Создайте объект формы с помощью класса построителя тестовых данных. Установите «допустимые» значения для полей заголовка и описания созданного объекта.
  2. Создайте объект Todo, который возвращается при вызове метода add () интерфейса TodoService .
  3. Сконфигурируйте наш фиктивный объект так, чтобы он возвращал созданный объект Todo, когда вызывается его метод add (), а созданный объект формы задается в качестве параметра метода.
  4. Выполните POST- запрос к URL ‘todo / add’. Установите тип содержимого запроса «application / x-www-form-urlencoded». Убедитесь, что содержимое нашего объекта формы отправлено в теле запроса. Установите объект формы в сеанс.
  5. Убедитесь, что возвращается код состояния HTTP 302.
  6. Убедитесь, что имя возвращаемого представления «redirect: todo / {id}».
  7. Убедитесь, что запрос перенаправлен на url ‘/ todo / 1 ′.
  8. Убедитесь, что атрибут модели с именем id равен 1.
  9. Убедитесь, что сообщение обратной связи установлено.
  10. Убедитесь, что метод add () нашего фиктивного объекта вызывается только один раз и что объект формы был задан в качестве параметра метода.
  11. Убедитесь, что никакие другие методы фиктивного объекта не были вызваны во время нашего теста.

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

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
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
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;
 
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
@WebAppConfiguration
public class TodoControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private TodoService todoServiceMock;
 
    //Add WebApplicationContext field here
 
    //The setUp() method is omitted.
 
    @Test
    public void add_NewTodoEntry_ShouldAddTodoEntryAndRenderViewTodoEntryView() throws Exception {
        TodoDTO formObject = new TodoDTOBuilder()
                .description("description")
                .title("title")
                .build();
 
        Todo added = new TodoBuilder()
                .id(1L)
                .description(formObject.getDescription())
                .title(formObject.getTitle())
                .build();
 
        when(todoServiceMock.add(formObject)).thenReturn(added);
 
        mockMvc.perform(post("/todo/add")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(formObject))
                .sessionAttr("todo", formObject)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(view().name("redirect:todo/{id}"))
                .andExpect(redirectedUrl("/todo/1"))
                .andExpect(model().attribute("id", is("1")))
                .andExpect(flash().attribute("feedbackMessage", is("Todo entry: title was added.")));
 
        verify(todoServiceMock, times(1)).add(formObject);
        verifyNoMoreInteractions(todoServiceMock);
    }
}

Резюме

Теперь мы написали некоторые модульные тесты для «обычных» методов контроллера с использованием среды Spring MVC Test. Этот урок научил есть четыре вещи:

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

Следующая часть этого руководства описывает, как мы можем написать модульные тесты для REST API.

PS Пример приложения этого блога доступен на Github . Я рекомендую вам проверить это, потому что у него есть некоторые модульные тесты, которые не были описаны в этом посте.