Статьи

Модульное тестирование контроллеров Spring MVC: REST API

Spring MVC предоставляет простой способ создания REST API. Однако написание комплексных и быстрых модульных тестов для этих API было проблематичным. Выпуск Spring MVC Test Framework дал нам возможность писать модульные тесты, которые читабельны, всеобъемлющи и быстры.

В этой записи блога описывается, как мы можем написать модульные тесты для REST API, используя среду Spring MVC Test. В этом посте мы напишем модульные тесты для методов контроллера, которые предоставляют функции CRUD для записей todo.

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

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

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

  • Hamcrest 1.3 ( Хамкрест-все ). Мы используем совпадения Hamcrest, когда пишем утверждения для ответов.
  • Junit 4.11. Нам нужно исключить зависимость от hamcrest-core, потому что мы уже добавили зависимость hamcrest-all .
  • Мокито 1.9.5 ( ядро мокито ). Мы используем Mockito в качестве нашей библиотеки для насмешек.
  • Весенний тест 3.2.3. РЕЛИЗ
  • JsonPath 0.8.1 ( json-path и json-path-assert ). Мы используем JsonPath, когда пишем утверждения для документов JSON, возвращаемых нашим REST API.

Соответствующие объявления зависимостей выглядят следующим образом:

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>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>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>0.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path-assert</artifactId>
    <version>0.8.1</version>
    <scope>test</scope>
</dependency>

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

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

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

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

Тем не менее, есть одна вещь, которую мы должны рассмотреть здесь.

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

Это имеет смысл, если мы реализуем «нормальное» приложение Spring MVC. Однако, если мы реализуем REST API, мы хотим преобразовать исключения в коды состояния HTTP. Это поведение обеспечивается классом ResponseStatusExceptionResolver, который включен по умолчанию.

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

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

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

Прежде чем мы начнем писать модульные тесты для нашего REST API, нам нужно понять две вещи:

Далее мы увидим платформу Spring MVC Test в действии и напишем модульные тесты для следующих методов контроллера:

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

Получить записи Todo

Первый метод контроллера возвращает список записей todo, найденных в базе данных. Давайте начнем с рассмотрения реализации этого метода.

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

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

  1. Он обрабатывает запросы GET, отправленные на URL / api / todo.
  2. Он получает список объектов Todo , вызывая метод findAll () интерфейса TodoService . Этот метод возвращает все записи, которые хранятся в базе данных. Эти записи todo всегда возвращаются в одном и том же порядке.
  3. Преобразует полученный список в список объектов TodoDTO .
  4. Возвращает список, содержащий объекты TodoDTO .

Соответствующая часть класса 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
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
import java.util.ArrayList;
import java.util.List;
 
@Controller
public class TodoController {
 
    private TodoService service;
 
    @RequestMapping(value = "/api/todo", method = RequestMethod.GET)
    @ResponseBody
    public List<TodoDTO> findAll() {
        List<Todo> models = service.findAll();
        return createDTOs(models);
    }
 
    private List<TodoDTO> createDTOs(List<Todo> models) {
        List<TodoDTO> dtos = new ArrayList<>();
 
        for (Todo model: models) {
            dtos.add(createDTO(model));
        }
 
        return dtos;
    }
 
    private TodoDTO createDTO(Todo model) {
        TodoDTO dto = new TodoDTO();
 
        dto.setId(model.getId());
        dto.setDescription(model.getDescription());
        dto.setTitle(model.getTitle());
 
        return dto;
    }
}

Когда возвращается список объектов TodoDTO , Spring MVC преобразует этот список в документ JSON, который содержит коллекцию объектов. Возвращенный документ JSON выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
[
    {
        "id":1,
        "description":"Lorem ipsum",
        "title":"Foo"
    },
    {
        "id":2,
        "description":"Lorem ipsum",
        "title":"Bar"
    }
]

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

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

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

  1. Создайте тестовые данные, которые возвращаются при вызове метода findAll () интерфейса TodoService . Мы создаем тестовые данные, используя класс построителя тестовых данных .
  2. Сконфигурируйте наш фиктивный объект так, чтобы он возвращал созданные тестовые данные, когда вызывается его метод findAll () .
  3. Выполните запрос GET для URL ‘/ api / todo’.
  4. Убедитесь, что код состояния HTTP 200 возвращается.
  5. Убедитесь, что тип содержимого ответа — «application / json», а его набор символов — «UTF-8».
  6. Получите коллекцию записей задач с помощью выражения $ JsonPath и убедитесь, что возвращены две записи задач.
  7. Получите идентификатор , описание и заголовок первой записи задачи, используя выражения JsonPath $ [0] .id , $ [0] .description и $ [0] .title . Убедитесь, что верные значения возвращены.
  8. Получите идентификатор , описание и заголовок второй записи задачи, используя выражения JsonPath $ [1] .id , $ [1] .description и $ [1] .title . Убедитесь, что верные значения возвращены.
  9. Убедитесь, что метод findAll () интерфейса TodoService вызывается только один раз.
  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
import org.junit.Test;
import org.junit.runner.RunWith;
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 java.util.Arrays;
 
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
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 findAll_TodosFound_ShouldReturnFoundTodoEntries() 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("/api/todo"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id", is(1)))
                .andExpect(jsonPath("$[0].description", is("Lorem ipsum")))
                .andExpect(jsonPath("$[0].title", is("Foo")))
                .andExpect(jsonPath("$[1].id", is(2)))
                .andExpect(jsonPath("$[1].description", is("Lorem ipsum")))
                .andExpect(jsonPath("$[1].title", is("Bar")));
 
        verify(todoServiceMock, times(1)).findAll();
        verifyNoMoreInteractions(todoServiceMock);
    }
}

Наш модульный тест использует константу с именем APPLICATION_JSON_UTF8, которая объявлена ​​в классе TestUtil . Значением этой константы является объект MediaType, тип содержимого которого «application / json», а набор символов — «UTF-8».

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

1
2
3
4
5
6
7
public class TestUtil {
 
    public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(),                      
    Charset.forName("utf8")                   
  );
}

Получить Todo Entry

Второй метод контроллера, который мы должны протестировать, возвращает информацию об одной записи todo. Давайте выясним, как реализован этот метод контроллера.

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

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

  1. Он обрабатывает запросы GET, отправленные на URL ‘/ api / todo / {id}’. {Id} — это переменная пути, которая содержит идентификатор запрошенной записи todo.
  2. Он получает запрошенную запись todo путем вызова метода findById () интерфейса TodoService и передает идентификатор запрошенной записи todo в качестве параметра метода. Этот метод возвращает найденную запись todo. Если запись todo не найдена, этот метод генерирует исключение TodoNotFoundException .
  3. Он превращает объект Todo в объект TodoDTO .
  4. Возвращает созданный объект TodoDTO .

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

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
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
@Controller
public class TodoController {
 
    private TodoService service;
 
    @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.GET)
    @ResponseBody
    public TodoDTO findById(@PathVariable("id") Long id) throws TodoNotFoundException {
        Todo found = service.findById(id);
        return createDTO(found);
    }
 
    private TodoDTO createDTO(Todo model) {
        TodoDTO dto = new TodoDTO();
 
        dto.setId(model.getId());
        dto.setDescription(model.getDescription());
        dto.setTitle(model.getTitle());
 
        return dto;
    }
}

Документ JSON, который возвращается клиенту, выглядит следующим образом:

1
2
3
4
5
{
    "id":1,
    "description":"Lorem ipsum",
    "title":"Foo"
}

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

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

В нашем примере приложения есть класс обработчика исключений, который обрабатывает специфичные для приложения исключения, создаваемые нашими классами контроллеров. Этот класс имеет метод обработчика исключений, который вызывается, когда выбрасывается исключение TodoNotFoundException . Реализация этого способа записывает новое сообщение журнала в файл журнала и гарантирует, что код состояния HTTP 404 будет отправлен обратно клиенту.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
 
@ControllerAdvice
public class RestErrorHandler {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class);
 
    @ExceptionHandler(TodoNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public void handleTodoNotFoundException(TodoNotFoundException ex) {
        LOGGER.debug("handling 404 error on a todo entry");
    }
}

Мы должны написать два модульных теста для этого метода контроллера:

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

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

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

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

  1. Сконфигурируйте наш фиктивный объект, чтобы он выдавал исключение TodoNotFoundException, когда вызывается его метод findById () и идентификатор запрошенной записи todo равен 1L.
  2. Выполните запрос GET для URL ‘/ api / todo / 1’.
  3. Убедитесь, что возвращается код состояния HTTP 404.
  4. Убедитесь, что метод findById () интерфейса TodoService вызывается только один раз, используя правильный параметр метода (1L).
  5. Убедитесь, что никакие другие методы интерфейса TodoService не вызываются во время этого теста.

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

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
import org.junit.Test;
import org.junit.runner.RunWith;
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 static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
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_ShouldReturnHttpStatusCode404() throws Exception {
        when(todoServiceMock.findById(1L)).thenThrow(new TodoNotFoundException(""));
 
        mockMvc.perform(get("/api/todo/{id}", 1L))
                .andExpect(status().isNotFound());
 
        verify(todoServiceMock, times(1)).findById(1L);
        verifyNoMoreInteractions(todoServiceMock);
    }
}

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

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

  1. Создайте объект Todo, который возвращается, когда вызывается наш сервисный метод. Мы создаем этот объект с помощью нашего тестового сборщика данных.
  2. Сконфигурируйте наш фиктивный объект так, чтобы он возвращал созданный объект Todo, когда вызывается его метод findById () с использованием параметра метода 1L.
  3. Выполните запрос GET для URL ‘/ api / todo / 1’.
  4. Убедитесь, что код состояния HTTP 200 возвращается.
  5. Убедитесь, что тип содержимого ответа — «application / json», а его набор символов — «UTF-8».
  6. Получите идентификатор записи todo с помощью выражения JsonPath $ .id и убедитесь, что идентификатор равен 1.
  7. Получите описание записи todo с помощью выражения JsonPath $ .description и убедитесь, что описание «Lorem ipsum».
  8. Получите заголовок записи todo с помощью выражения JsonPath $ .title и убедитесь, что заголовок «Foo».
  9. Убедитесь, что метод findById () интерфейса TodoService вызывается только один раз, используя правильный параметр метода (1L).
  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
import org.junit.Test;
import org.junit.runner.RunWith;
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 static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 
@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_ShouldReturnFoundTodoEntry() throws Exception {
        Todo found = new TodoBuilder()
                .id(1L)
                .description("Lorem ipsum")
                .title("Foo")
                .build();
 
        when(todoServiceMock.findById(1L)).thenReturn(found);
 
        mockMvc.perform(get("/api/todo/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.description", is("Lorem ipsum")))
                .andExpect(jsonPath("$.title", is("Foo")));
 
        verify(todoServiceMock, times(1)).findById(1L);
        verifyNoMoreInteractions(todoServiceMock);
    }
}

Добавить новую запись Todo

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

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

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

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

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

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
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
import javax.validation.Valid;
 
@Controller
public class TodoController {
 
    private TodoService service;
 
    @RequestMapping(value = "/api/todo", method = RequestMethod.POST)
    @ResponseBody
    public TodoDTO add(@Valid @RequestBody TodoDTO dto) {
        Todo added = service.add(dto);
        return createDTO(added);
    }
 
    private TodoDTO createDTO(Todo model) {
        TodoDTO dto = new TodoDTO();
 
        dto.setId(model.getId());
        dto.setDescription(model.getDescription());
        dto.setTitle(model.getTitle());
 
        return dto;
    }
}

Класс 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.
}

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

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

Если проверка не пройдена, наш компонент-обработчик ошибок гарантирует, что

  1. Код состояния HTTP 400 возвращается клиенту.
  2. Ошибки проверки возвращаются клиенту в виде документа JSON.

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

Однако нам нужно знать, какой тип документа JSON возвращается клиенту, если проверка не удалась. Эта информация приводится в следующем.

Если заголовок и описание объекта TodoDTO слишком длинные, клиенту возвращается следующий документ JSON:

01
02
03
04
05
06
07
08
09
10
11
12
{
    "fieldErrors":[
        {
            "path":"description",
            "message":"The maximum length of the description is 500 characters."
        },
        {
            "path":"title",
            "message":"The maximum length of the title is 100 characters."
        }
    ]
}

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

С другой стороны, если проверка не завершится неудачей, наш метод контроллера возвращает следующий документ JSON клиенту:

1
2
3
4
5
{
    "id":1,
    "description":"description",
    "title":"todo"
}

Мы должны написать два модульных теста для этого метода контроллера:

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

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

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

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

  1. Создайте заголовок, который имеет 101 символ.
  2. Создайте описание, которое имеет 501 символов.
  3. Создайте новый объект TodoDTO с помощью нашего построителя тестовых данных. Установите заголовок и описание объекта.
  4. Выполните POST- запрос к URL ‘/ api / todo’. Установите тип содержимого запроса «application / json». Установите набор символов запроса «UTF-8». Преобразуйте созданный объект TodoDTO в байты JSON и отправьте его в теле запроса.
  5. Убедитесь, что код состояния HTTP 400 возвращается.
  6. Убедитесь, что тип содержимого ответа — «application / json», а тип содержимого — «UTF-8».
  7. Извлеките ошибки поля, используя выражение JsonPath $ .fieldErrors, и убедитесь, что возвращены две ошибки поля.
  8. Извлеките все доступные пути с помощью выражения JsonPath $ .fieldErrors [*]. Path и убедитесь, что найдены ошибки полей в полях title и description .
  9. Извлеките все доступные сообщения об ошибках с помощью выражения JsonPath $ .fieldErrors [*]. Message и убедитесь, что найдены сообщения об ошибках в полях title и description .
  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
import org.junit.Test;
import org.junit.runner.RunWith;
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 static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 
@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_TitleAndDescriptionAreTooLong_ShouldReturnValidationErrorsForTitleAndDescription() throws Exception {
        String title = TestUtil.createStringWithLength(101);
        String description = TestUtil.createStringWithLength(501);
 
        TodoDTO dto = new TodoDTOBuilder()
                .description(description)
                .title(title)
                .build();
 
        mockMvc.perform(post("/api/todo")
                .contentType(TestUtil.APPLICATION_JSON_UTF8)
                .content(TestUtil.convertObjectToJsonBytes(dto))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.fieldErrors", hasSize(2)))
                .andExpect(jsonPath("$.fieldErrors[*].path", containsInAnyOrder("title", "description")))
                .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder(
                        "The maximum length of the description is 500 characters.",
                        "The maximum length of the title is 100 characters."
                )));
 
        verifyZeroInteractions(todoServiceMock);
    }
}

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

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

Исходный код класса 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
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
 
import java.io.IOException;
import java.nio.charset.Charset;
 
public class TestUtil {
 
    public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
 
    public static byte[] convertObjectToJsonBytes(Object object) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper.writeValueAsBytes(object);
    }
 
    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. Создайте новый объект TodoDTO с помощью нашего построителя тестовых данных. Установите «допустимые» значения для полей заголовка и описания .
  2. Создайте объект Todo, который возвращается при вызове метода add () интерфейса TodoService .
  3. Сконфигурируйте наш фиктивный объект так, чтобы он возвращал созданный объект Todo, когда вызывается его метод add () и объект TodoDTO задается в качестве параметра.
  4. Выполните POST- запрос к URL ‘/ api / todo’. Установите тип содержимого запроса «application / json». Установите набор символов запроса «UTF-8». Преобразуйте созданный объект TodoDTO в байты JSON и отправьте его в теле запроса.
  5. Убедитесь, что код состояния HTTP 200 возвращается.
  6. Убедитесь, что тип содержимого ответа — «application / json», а тип содержимого — «UTF-8».
  7. Получите идентификатор возвращенной записи todo с помощью выражения JsonPath $ .id и убедитесь, что идентификатор равен 1.
  8. Получите описание возвращенной записи todo с помощью выражения JsonPath $ .description и убедитесь, что описание «описание».
  9. Получите заголовок возвращенной записи todo с помощью выражения JsonPath $ .title и убедитесь, что заголовок равен «title».
  10. Создайте объект ArgumentCaptor, который может захватывать объекты TodoDTO .
  11. Убедитесь, что метод add () интерфейса TodoService вызывается только один раз, и запишите объект, заданный в качестве параметра.
  12. Убедитесь, что другие методы нашего фиктивного объекта не вызываются во время нашего теста.
  13. Убедитесь, что идентификатор захваченного объекта TodoDTO равен нулю.
  14. Убедитесь, что описание захваченного объекта TodoDTO — «описание».
  15. Убедитесь, что заголовок захваченного объекта TodoDTO — «title».

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

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.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 static junit.framework.Assert.assertNull;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 
@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_ShouldAddTodoEntryAndReturnAddedEntry() throws Exception {
        TodoDTO dto = new TodoDTOBuilder()
                .description("description")
                .title("title")
                .build();
 
        Todo added = new TodoBuilder()
                .id(1L)
                .description("description")
                .title("title")
                .build();
 
        when(todoServiceMock.add(any(TodoDTO.class))).thenReturn(added);
 
        mockMvc.perform(post("/api/todo")
                .contentType(TestUtil.APPLICATION_JSON_UTF8)
                .content(TestUtil.convertObjectToJsonBytes(dto))
        )
                .andExpect(status().isOk())
                .andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.description", is("description")))
                .andExpect(jsonPath("$.title", is("title")));
 
        ArgumentCaptor<TodoDTO> dtoCaptor = ArgumentCaptor.forClass(TodoDTO.class);
        verify(todoServiceMock, times(1)).add(dtoCaptor.capture());
        verifyNoMoreInteractions(todoServiceMock);
 
        TodoDTO dtoArgument = dtoCaptor.getValue();
        assertNull(dtoArgument.getId());
        assertThat(dtoArgument.getDescription(), is("description"));
        assertThat(dtoArgument.getTitle(), is("title"));
    }
}

Резюме

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

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

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