В первом блоге этой мини-серии была представлена среда тестирования Spring MVC и продемонстрировано ее использование при модульном тестировании классов контроллеров Spring MVC в качестве контроллеров, а не POJO. Пришло время поговорить об использовании фреймворка для интеграционного тестирования.
Под «интеграционным тестированием» я подразумеваю загрузку контекста Spring в тестовую среду, чтобы контроллер мог работать со своими соавторами в сквозных тестах.
Опять же, я собираюсь написать тест для FacebookPostsController
из моего проекта Spring Social Facebook, и тест, как вы можете ожидать, будет интеграционной тестовой версией моего класса FacebookPostsControllerTest
. Если вам нужно увидеть код FacebookPostsController
или оригинальный код FacebookPostsControllerTest
, взгляните на мой последний блог . Для полного ознакомления с кодом FacebookPostsController
см. Блог Spring Social Facebook .
Первым шагом в создании интеграционного теста является загрузка контекста Spring в вашу тестовую среду. Это делается путем добавления следующих аннотаций в класс FacebookPostsControllerTest
:
- @RunWith ( SpringJUnit4ClassRunner.class )
- @WebAppConfiguration
- @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.