Статьи

Начало работы с Spring MVC Test Framework — часть 2

В первом блоге этой мини-серии была представлена ​​среда тестирования Spring MVC и продемонстрировано ее использование при модульном тестировании классов контроллеров Spring MVC в качестве контроллеров, а не POJO. Пришло время поговорить об использовании фреймворка для интеграционного тестирования.

Под «интеграционным тестированием» я подразумеваю загрузку контекста Spring в тестовую среду, чтобы контроллер мог работать со своими соавторами в сквозных тестах.

Опять же, я собираюсь написать тест для FacebookPostsController из моего проекта Spring Social Facebook, и тест, как вы можете ожидать, будет интеграционной тестовой версией моего класса FacebookPostsControllerTest . Если вам нужно увидеть код FacebookPostsController или оригинальный код FacebookPostsControllerTest , взгляните на мой последний блог . Для полного ознакомления с кодом FacebookPostsController см. Блог Spring Social Facebook .

Первым шагом в создании интеграционного теста является загрузка контекста Spring в вашу тестовую среду. Это делается путем добавления следующих аннотаций в класс FacebookPostsControllerTest :

  1. @RunWith ( SpringJUnit4ClassRunner.class )
  2. @WebAppConfiguration
  3. @ContextConfiguration ( «файл-имена»)
1
2
3
4
5
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml",
    "file:src/main/webapp/WEB-INF/spring/data.xml" })
public class FacebookPostsControllerTest {

В @RunWith ( SpringJUnit4ClassRunner.class ) или @ContextConfiguration («имена-файлов») нет ничего нового, поскольку они существовали со времен Spring 2.5, и если вы являетесь разработчиком Spring, то, вероятно, вы использовали их в своих интеграционных тестах до , Новичком является @WebAppConfiguration .

Эти аннотации работают вместе для настройки вашей тестовой среды. @RunWith указывает JUnit запустить тест с использованием бегуна классов Spring JUnit. @WebAppConfiguration сообщает SpringJUnit4ClassRunner что ApplicationContext для загрузки для интеграционного теста должен быть WebApplicationContext , тогда как @ContextConfiguration используется для указания, какой файл XML загружен и откуда.

В этом случае я загружаю файлы «servlet-context.xml» и «data.xml» проекта. Файл «servlet-context.xml» содержит все стандартные фрагменты, которые вы ожидаете от веб-приложения Spring, такие как <annotation-driven / / и преобразователи представлений, тогда как «data.xml» содержит конфигурацию базы данных, используемую Весенние социальные компоненты приложения. Следует отметить, что я намеренно использую
Конфигурационные файлы псевдопроизводства, так как я хочу запустить сквозной тест интеграции с доступом к файловой системе, базе данных и т. д.

Это всего лишь пример кода, и вы обычно не затрагивали бы производственные базы данных или другие связанные ресурсы в своих интеграционных тестах. Обычно вы настраиваете свое приложение для доступа к базам данных тестирования интеграции и другим ресурсам. Одним из способов решения этой проблемы является создание тестового XML-файла конфигурации; однако, как я видел в одном проекте, не создавайте отдельный тестовый XML-файл для каждого модуля Maven в вашем проекте; причина в том, что когда вы вносите изменения в свой код, вы в конечном итоге изменяете целую кучу конфигурационных файлов, чтобы заставить интеграционные тесты работать снова, что скучно и отнимает много времени. Лучше всего иметь одну версию XML-конфигурации и использовать профили Spring для настройки приложения для различных сред. Если вы решите использовать профили, вам также необходимо добавить @ActiveProfiles(“profile-name”) к трем другим аннотациям, перечисленным выше; однако, это выходит за рамки этого блога.

Предполагая, что вы используете автопроводку и правильно настроили <context:component-scan /> , следующий шаг — добавить следующую переменную экземпляра в ваш тестовый класс:

1
2
  @Autowired
  private WebApplicationContext wac;

Это говорит Spring, чтобы внедрить WebApplicationContext он создал ранее, в ваш тест. Затем это используется в очень простом однострочном методе setup() :

1
2
3
4
5
  @Before
  public void setUp() throws Exception {
 
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }

Как и в «автономной» / «программной» версии этого теста, цель метода setup() — создать экземпляр mockMvc а затем использовать его для выполнения тестов. Разница здесь в том, что он создается просто с помощью WebApplicationContext в качестве аргумента для MockMvcBuilders .

Разобрав метод setup() , я должен написать тест, и я собираюсь переписать testShowPostsForUser_user_is_not_signed_in() из моего последнего блога в качестве интеграционного теста. Сюрпризом здесь является то, что код намного проще, чем предыдущая версия JUnit:

1
2
3
4
5
6
7
  @Test
  public void testShowPostsForUser_user_is_not_signed_in() throws Exception {
 
    ResultActions resultActions = mockMvc.perform(get("/posts").accept(MediaType.ALL));
    resultActions.andExpect(status().isOk());
    resultActions.andExpect(view().name("signin"));
  }

Если вы сравните этот код с кодом testShowPostsForUser_user_is_not_signed_in() в моем предыдущем блоге, вы увидите, что он почти идентичен. Разница лишь в том, что нет необходимости устанавливать какие-либо фиктивные объекты.

На этом этапе я собирался продемонстрировать интеграционную тестовую версию своего теста testShowPostsForUser_user_is_signed_in , но это оказалось немного сложнее. Причина этого заключается в том, что для получения списка своих сообщений в Facebook пользователь должен войти в свою учетную запись Facebook, а это означает, что необходимо выполнить несколько последовательных вызовов к серверу, прежде чем нужный объект HttpServletRequest окажется в нужном месте. состояние, чтобы облегчить вызов в Facebook, чтобы получить список сообщений. Это казалось слишком сложным для примера кода, и это то, что я не хотел бы делать на
реальный проект.

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

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

Хорошим примером независимого атомарного вызова сервера является вызов REST для testConfirmPurchases_selection_1_returns_a_hat(...) для класса OrderController взятого из моего Spring MVC, Ajax и JSON, часть 2 — Блог кода на стороне сервера . Этот код, полностью описанный в блоге Ajax , запрашивает подтверждение покупки, которое возвращается как JSON.

Код OrderController который возвращает JSON, приведен ниже:

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
  /**
   * Create an order form for user confirmation
   */
  @RequestMapping(value = "/confirm", method = RequestMethod.POST)
  public @ResponseBody
  OrderForm confirmPurchases(@ModelAttribute("userSelections") UserSelections userSelections) {
 
    logger.debug("Confirming purchases...");
    OrderForm orderForm = createOrderForm(userSelections.getSelection());
    return orderForm;
  }
 
  private OrderForm createOrderForm(List<String> selections) {
 
    List<Item> items = findItemsInCatalogue(selections);
    String purchaseId = getPurchaseId();
 
    OrderForm orderForm = new OrderForm(items, purchaseId);
    return orderForm;
  }
 
  private List<Item> findItemsInCatalogue(List<String> selections) {
 
    List<Item> items = new ArrayList<Item>();
    for (String selection : selections) {
      Item item = catalogue.findItem(Integer.valueOf(selection));
      items.add(item);
    }
    return items;
  }
 
  private String getPurchaseId() {
    return UUID.randomUUID().toString();
  }

Пока возвращается JSON, он выглядит примерно так:

1
2
3
{"items":[{"id":1,"description":"description","name":"name","price":1.00},
    {"id":2,"description":"description2","name":"name2","price":2.00}],
    "purchaseId":"aabf118e-abe9-4b59-88d2-0b897796c8c0"}

Код, который тестирует testConfirmPurchases_selection_1_returns_a_hat(...) , показан ниже в подробном стиле.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
  @Test
  public void testConfirmPurchases_selection_1_returns_a_hat() throws Exception {
 
    final String mediaType = "application/json;charset=UTF-8";
 
    MockHttpServletRequestBuilder postRequest = post("/confirm");
    postRequest = postRequest.param("selection", "1");
 
    ResultActions resultActions = mockMvc.perform(postRequest);
 
    resultActions.andDo(print());
    resultActions.andExpect(content().contentType(mediaType));
    resultActions.andExpect(status().isOk());
 
    // See http://goessner.net/articles/JsonPath/ for more on JSONPath
    ResultMatcher pathMatcher = jsonPath("$items[0].description").value("A nice hat");
    resultActions.andExpect(pathMatcher);
  }

Приведенный выше код не так, как парни из Spring предпочли бы, чтобы вы написали это; однако в подробном формате легче обсуждать, что происходит. Структура этого метода аналогична testShowPostsForUser_user_is_signed_in(...) описанному в части 1. Первым шагом является создание объекта MockHttpServletRequestBuilder типа MockHttpServletRequestBuilder с использованием статического MockMvcRequestBuilders.post(...) . Параметр "selection" со значением "1" добавляется в результирующий объект.

Затем postRequest передается mockMvc.perform(...) и ResultActions объект ResultActions .

Затем объект ResultActions проверяется с помощью andExpect(...) для проверки как статуса HTTP (ok = 200), так и того, что тип содержимого — "application/json;charset=UTF-8" .

Кроме того, я добавил вызов метода andDo(print()) для отображения состояния объектов HttpServletRequest и HttpServletResponse . Результат этого вызова показан ниже:

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
MockHttpServletRequest:
         HTTP Method = POST
         Request URI = /confirm
          Parameters = {selection=[1]}
             Headers = {}
 
             Handler:
                Type = com.captaindebug.store.OrderController
              Method = public com.captaindebug.store.beans.OrderForm com.captaindebug.store.OrderController.confirmPurchases(com.captaindebug.store.beans.UserSelections)
 
  Resolved Exception:
                Type = null
 
        ModelAndView:
           View name = null
                View = null
               Model = null
 
            FlashMap:
 
MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json;charset=UTF-8]}
        Content type = application/json;charset=UTF-8
                Body = {"items":[{"id":1,"description":"A nice hat","name":"Hat","price":12.34}],"purchaseId":"d1d0eba6-51fa-415f-ac4e-8fa2eaeaaba9"}
       Forwarded URL = null
      Redirected URL = null
             Cookies = []

В одном последнем тесте используется статический MockMvcResultMatchers.jsonPath(...) чтобы проверить, что путь JSON для "$items[0].description" имеет значение "A nice hat" . Чтобы использовать статический метод jsonPath(...) вы должны включить модуль JSON Path в свой файл POM.xml для анализа JSON.

1
2
3
4
5
6
<dependency>
  <groupId>com.jayway.jsonpath</groupId>
  <artifactId>json-path</artifactId>
  <version>0.8.1</version>
  <scope>test</scope>
 </dependency>

JSonPath — это способ выборочного извлечения полей из данных JSon. Он основан на идее XPath XML.

Очевидно, что писать тесты не нужно — это многословный стиль, который я использовал выше. Код ниже показывает тот же код, который разработали ребята из Spring:

1
2
3
4
5
6
7
8
  @Test
  public void testConfirmPurchases_spring_style() throws Exception {
 
    mockMvc.perform(post("/confirm").param("selection", "1")).andDo(print())
        .andExpect(content().contentType("application/json;charset=UTF-8"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$items[0].description").value("A nice hat"));
  }

Итак, это все, что нужно сделать. Напомним, что идея состоит в том, чтобы добавить соответствующие аннотации к вашему модульному тесту, чтобы Spring WebApplicationContext вашу конфигурацию XML для создания WebApplicationContext . Затем он внедряется в ваш тест и передается в среду Spring MVC Test в качестве параметра при создании mockMvc . Затем тесты пишутся с идеей передачи надлежащим образом сконструированного объекта mockMvc.perform(...) методу mockMvc.perform(...) , возвращаемое значение которого затем утверждается для прохождения или неудачи теста.

Код для этого блога доступен на GitHub: https://github.com/roghughe/captaindebug/ в проектах Facebook и Ajax-JSON.