Недавно в основную среду Spring была добавлена среда Spring MVC Test Framework, которая, по утверждению Guys at Spring, является «первоклассной поддержкой JUnit для тестирования клиентского и серверного кода Spring MVC через свободный API» 1 . В этом и моем следующем блоге я собираюсь взглянуть на среду тестирования MVC Spring и применить ее к некоторым из моих существующих примеров кода, чтобы выяснить, выполняет ли он то, что говорит на жестяной коробке.
API был разработан с двумя способами настройки тестов на стороне сервера. Это, во-первых, с файлом контекста Spring, а во-вторых, программно без файла контекста. Парни в Весне называют программный метод «автономным» режимом.
Настройка программных тестов, похоже, больше похожа на модульное тестирование и лучше всего используется для модульного тестирования определенного класса контроллера в отрыве от его соавторов. С другой стороны, загрузка файла контекста Spring действительно является интеграционным тестированием и больше подходит для сквозных тестов.
Вы можете найти полный список блогов по методикам тестирования здесь .
Если вы похожи на меня, то вы уже будете использовать существующую инфраструктуру, такую как Mockito или Easymock, для тестирования ваших контроллеров. Обычный подход Mockito / Easymock заключается в создании экземпляра вашего контроллера, внедрении ложных или заглушенных зависимостей и последующем вызове тестируемого метода, отмечая возвращаемое значение или проверяя вызовы ложных методов.
Среда Spring Mvc Test использует другой подход к другим фиктивным средам, поскольку она загружает Spring DispatcherServlet для эмуляции работы веб-контейнера. Затем тестируемый контроллер загружается в контекст Spring и обращается к DispatcherServlet так же, как и в «реальной жизни».
Преимущество этого подхода заключается в том, что он позволяет вам тестировать контроллер как контроллер, а не как POJO. Это означает, что аннотации контроллера обрабатываются и учитываются, проверка выполняется и методы вызываются в правильном порядке.
Согласны ли вы с этим подходом или нет, зависит от вашего взгляда на методы тестирования. Если вы придерживаетесь мнения, что каждый тестируемый вами класс / метод должен быть изолирован до n-й степени, а каждый тест — полностью атомарный, то, возможно, это не для вас. Если вы немного более прагматичны и можете увидеть преимущества тестирования контроллера как… контроллера, тогда эта среда может представлять интерес.
Несколько отличаясь в подходах к Mockito и Easymock, недостатком является то, что код выглядит иначе, чем в этих старых, более устоявшихся технологиях. Он в значительной степени опирается на шаблон компоновщика для построения соответствий, компоновщиков запросов и обработчиков, и как только вы освоите его, все это имеет смысл. Я подозреваю, что мотивация для использования шаблона построителя состоит в том, чтобы упростить настройку фиктивного объекта HttpServletRequest
и опрос фиктивных объектов HttpServletResponse
, что по определению может быть довольно сложным.
В этом блоге я собираюсь взглянуть на программную / автономную технику Spring API, сравнивая ее с аналогичным модульным тестом на основе Mockito.
Чтобы ускорить процесс, я использую метод Blue Peter «вот тот, который я подготовил ранее», и беру FacebookPostsController
из моего блога Facebook, для которого я напишу два класса модульных тестов: первый с использованием Mockito, а другой с использованием Spring Mvc Test 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
|
@Controller public class FacebookPostsController { private static final Logger logger = LoggerFactory .getLogger(FacebookPostsController. class ); @Autowired private SocialContext socialContext; @RequestMapping (value = "posts" , method = RequestMethod.GET) public String showPostsForUser(HttpServletRequest request, HttpServletResponse response, Model model) throws Exception { String nextView; if (socialContext.isSignedIn(request, response)) { List<Post> posts = retrievePosts(); model.addAttribute( "posts" , posts); nextView = "show-posts" ; } else { nextView = "signin" ; } return nextView; } private List<Post> retrievePosts() { Facebook facebook = socialContext.getFacebook(); FeedOperations feedOps = facebook.feedOperations(); List<Post> posts = feedOps.getHomeFeed(); logger.info( "Retrieved " + posts.size() + " posts from the Facebook authenticated user" ); return posts; } } |
Я не буду вдаваться в подробности этого кода, так как он доступен в блоге Facebook ; однако, чтобы подвести итог, пример приложения Facebook получает доступ к учетной записи Facebook пользователя и отображает его ленту новостей в примере приложения. Для этого FacebookPostsController
проверяет класс SocialContext
чтобы выяснить, вошел ли пользователь в свою учетную запись Facebook. Если пользователь вошел в свою учетную запись Facebook, то контроллер извлекает сообщения пользователя и добавляет их в модель для отображения. С другой стороны, если пользователь не вошел в систему, он направляется на страницу входа.
Каждый из двух классов модульных тестов будет содержать три открытых метода:
setup()
, testShowPostsForUser_user_is_not_signed_in
и testShowPostsForUser_user_is_signed_in
каждый из которых я рассмотрю по очереди.
Как и следовало ожидать, тесты testShowPostsForUser_user_is_not_signed_in
и testShowPostsForUser_user_is_signed_in
используются для проверки случаев, когда пользователь не вошел в свою учетную запись Facebook.
«Стандартный» тест Мокито
1
2
3
4
5
6
7
8
|
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks( this ); instance = new FacebookPostsController(); ReflectionTestUtils.setField(instance, "socialContext" , socialContext); } |
Код установки довольно прост и содержит три простых шага:
- Инициализируйте фиктивные объекты, используя
MockitoAnnotations.initMocks(this)
. - Создайте новый экземпляр
FacebookPostsController
, тестируемый объект. -
SocialContext
фиктивныйSocialContext
вFacebookPostsController
.
1
2
3
4
5
6
7
8
|
@Test public void testShowPostsForUser_user_is_not_signed_in() throws Exception { when(socialContext.isSignedIn(request, response)).thenReturn( false ); String result = instance.showPostsForUser(request, response, model); assertEquals( "signin" , result); } |
Метод testShowPostsForUser_user_is_not_signed_in
настраивает макет SocialContext
для возврата false
при isSignedIn()
метода isSignedIn()
. Это означает, что все, что осталось сделать, — это утверждать, что метод showPostsForUser(...)
возвращает "signin"
направляя пользователя на страницу входа.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Test public void testShowPostsForUser_user_is_signed_in() throws Exception { when(socialContext.isSignedIn(request, response)).thenReturn( true ); when(socialContext.getFacebook()).thenReturn(facebook); when(facebook.feedOperations()).thenReturn(feedOps); List<Post> posts = Collections.emptyList(); when(feedOps.getHomeFeed()).thenReturn(posts); String result = instance.showPostsForUser(request, response, model); verify(model).addAttribute( "posts" , posts); assertEquals( "show-posts" , result); } |
testShowPostsForUser_user_is_signed_in
несколько сложнее. После настройки фиктивного SocialContext
для возврата true
при isSignedIn()
его isSignedIn()
также есть дополнительные четыре строки кода, которые гарантируют, что список posts
будет возвращен из фиктивного фида Facebook и добавлен в фиктивную Model
. После вызова showPostsForUser(...)
необходимо выполнить два дополнительных шага: проверить, что фиктивная Model
содержит список posts
и подтвердить, что возвращаемое значение из showPostsForUser(...)
равно "show-posts"
.
Затем код платформы Spring MVC Test; однако перед началом работы вам необходимо добавить следующую зависимость в ваш файл POM:
1
2
3
4
5
6
|
< dependency > < groupId >org.springframework</ groupId > < artifactId >spring-test</ artifactId > < version >${org.springframework-version}</ version > < scope >test</ scope > </ dependency > |
Весенний тест MVC
Версия среды Spring MVC Test выполняет те же два теста, но по-другому …
1
2
3
4
5
6
7
8
9
|
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks( this ); FacebookPostsController instance = new FacebookPostsController(); ReflectionTestUtils.setField(instance, "socialContext" , socialContext); mockMvc = MockMvcBuilders.standaloneSetup(instance).build(); } |
Если вы посмотрите на приведенный выше код, то увидите, что с точки зрения setup(...)
он выглядит довольно похоже на прямой код Mockito, приведенный выше. Как и в тесте на основе Mockito, первым шагом является инициализация фиктивных объектов с использованием
MockitoAnnotations.initMocks(this)
, за которым следует создание нового экземпляра FacebookPostsController
в который SocialContext
фиктивный SocialContext
. На этот раз, однако, FacebookPostsController
в статусе переведен в локальную переменную, поскольку весь смысл установки заключается в создании экземпляра Spring MockMvc
, который используется для выполнения тестов. mockMvc
создается путем вызова MockMvcBuilders.standaloneSetup(instance).build()
где instance
— это объект FacebookPostsController
мы тестируем.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testShowPostsForUser_user_is_not_signed_in() throws Exception { HttpServletRequest request = anyObject(); HttpServletResponse response = anyObject(); when(socialContext.isSignedIn(request, response)).thenReturn( false ); MockHttpServletRequestBuilder getRequest = get( "/posts" ).accept(MediaType.ALL); ResultActions results = mockMvc.perform(getRequest); results.andExpect(status().isOk()); results.andExpect(view().name( "signin" )); } |
Как и в версии testShowPostsForUser_user_is_not_signed_in
метод testShowPostsForUser_user_is_not_signed_in
настраивает mock SocialContext
для возврата false
при isSignedIn()
метода isSignedIn()
. На этот раз следующим шагом будет создание чего-то, называемого MockHttpServletRequestBuilder
используя статический метод.
MockMvcRequestBuilders.get(...)
и шаблон построителя. Это передается в метод mockMVC.perform(...)
где он используется для создания объекта MockHttpServletRequest
который используется для определения начальной точки для теста. В этом тесте все, что я сделал, это передал URL-адрес "/posts"
и установил вход как «любой» тип медиа. Вы можете настроить множество других атрибутов объекта запроса, используя такие методы, как contentType()
, contextPath()
, cookie()
и т. Д. Для получения дополнительной информации посмотрите Spring MockHttpServletRequest
для MockHttpServletRequest
Метод mockMvc.perform()
возвращает объект ResultActions
. Кажется, это обертка вокруг фактического MvcResult
. ResultsActions
— это ResultsActions
объект, используемый для ResultsActions
результата теста таким же образом, как и в JUnit assertEquals(...)
или в методах Mockito verify verify(..)
. В этом случае я проверяю, что итоговый статус Http в порядке (т.е. 200) и что следующее представление будет "signin"
.
Разница между этим тестом и версией только для Mockito заключается в том, что вы напрямую не тестируете результат вызова метода для вашего тестового экземпляра; вы тестируете объект HttpServletResponse
который генерирует вызов вашего метода.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Test public void testShowPostsForUser_user_is_signed_in() throws Exception { HttpServletRequest request = anyObject(); HttpServletResponse response = anyObject(); when(socialContext.isSignedIn(request, response)).thenReturn( true ); when(socialContext.getFacebook()).thenReturn(facebook); when(facebook.feedOperations()).thenReturn(feedOps); List<Post> posts = Collections.emptyList(); when(feedOps.getHomeFeed()).thenReturn(posts); mockMvc.perform(get( "/posts" ).accept(MediaType.ALL)).andExpect(status().isOk()) .andExpect(model().attribute( "posts" , posts)) .andExpect(view().name( "show-posts" )); } |
Версия Spring Test метода testShowPostsForUser_user_is_signed_in
очень похожа на версию Mockito тем, что тест готовится с помощью SocialContext.isSignedIn()
настроенного на возврат true
и feedOps.getHomeFeed()
настроенного на возврат списка posts
. Spring Mvc Test часть этого метода практически идентична описанной выше версии testShowPostsForUser_user_is_not_signed_in
, за исключением того, что на этот раз она проверяет имя следующего представления "show-posts"
а не "sign-in"
с помощью andExpect(view().name("show-posts")
. Стиль кода, который я здесь использовал, несколько отличается от стиля, который я использовал выше, и является стилем, предпочитаемым парнями в Spring. Вы можете найти много других примеров этого стиля на Github, если получите держите приложение Spring MVC Showcase .
Итак, что вы можете сделать из этого сравнения? Справедливости ради, это не настоящее сравнение — API-интерфейс Spring MVC Test, в то время как основанный на стандартной методике Mockito, использует другой подход, создавая среду, предназначенную исключительно для тестирования контроллеров Spring MVC в макете их собственной среды выполнения. ,
Будет ли это полезно для вас, я позволю вам решить. Преимущество заключается в том, что контроллеры рассматриваются как контроллеры, а не как POJO, что означает, что они тестируются более тщательно. С личной точки зрения, мне нравится идея загрузки конфигурационных файлов Spring и использования их для интеграционных тестов, о чем я расскажу в своем следующем блоге.
- 1 См. Http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/testing.html#spring-mvc-test-framework
- Код для этого блога доступен на GitHub: https://github.com/roghughe/captaindebug/tree/master/facebook