Интеграционные тесты могут быть медленными и ненадежными, поскольку они зависят от слишком большого количества компонентов в системе. До определенного момента это неизбежно: интеграционные тесты предназначены для проверки того, как каждая часть вашей системы играет с другими внутренними или внешними компонентами.
Однако мы можем улучшить некоторые интеграционные тесты, только раскручивая необходимые зависимости, а не всю систему. Давайте представим приложение, которое зависит от базы данных, стороннего REST API и очереди сообщений:
Предположим теперь, что мы хотели бы, чтобы наш интеграционный тест подтвердил поведение, которое включает в себя только вызовы REST API, но не вызовы базы данных или очереди сообщений. Чтобы привести конкретный пример, давайте предположим, что мы хотим проверить, правильно ли настроен наш клиент REST на тайм-аут через 3 секунды.
Все, что нам для этого нужно, — это маленький Controller
который будет издеваться над REST API, ожидая, прежде чем вернуть ответ клиенту REST. Время ожидания будет передано в качестве параметра в строке запроса.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
@Profile ( "restTemplateTimeout" ) @RestController @RequestMapping (value = "/test" ) public class DelayedWebServerController { @RequestMapping (value = "/delayRestTemplate" , method = GET) public String answerWithDelay( @RequestParam Long waitTimeMs) { if (waitTimeMs > 0 ) { try { Thread.sleep(waitTimeMs); } catch (InterruptedException e) { throw new RuntimeException(e); } } return "Delayed Result" ; } } |
Для @Profile
используется аннотация @Profile
? Если мы добавим этот контроллер в наш стандартный контекст приложения, это будет иметь несколько недостатков
- Тест будет медленным: нам нужно запустить только один контроллер, а не все
- Наш контроллер будет выбран Spring и введен во все остальные интеграционные тесты, замедляя каждый интеграционный тест и, возможно, наступая на трудности другого теста
DelayedWebServerController
альтернативой было бы раскрутить минимальное приложение Spring Boot, выставив только наш DelayedWebServerController
. Мы также скажем Spring Boot сканировать только те пакеты, которые нам интересны, и исключить автоконфигурацию, связанную с постоянством, поскольку она не нужна для ускорения контроллера. Это делается в классе Configuration
как этот:
1
2
3
4
5
6
7
8
|
@Profile ( "restTemplateTimeout" ) @Configuration @EnableAutoConfiguration ( exclude = {DataSourceAutoConfiguration. class , HibernateJpaAutoConfiguration. class }) @ComponentScan (basePackages = "my.application.resttemplate.timeout" ) public class DelayedWebServerConfiguration { //The class is empty and only used to support the annotations } |
Конфигурация контекста Spring может привести к путанице, давайте посмотрим на аннотации одну за другой:
-
@Profile
: сообщает Spring, что эту конфигурацию следует использовать только тогда, когдаrestTemplateTimeout
профильrestTemplateTimeout
. Далее в этой статье мы увидим, как мы включаем этот профиль для конкретного интеграционного теста. Именно эта аннотация предотвращает выбор конфигурации другими несвязанными интеграционными тестами. Обратите внимание, что нашDelayedWebServerController
идентично аннотирован. -
@Configuration
: стандартная аннотация,@Configuration
Spring, что это класс конфигурации контекста. -
@EnableAutoConfiguration
: здесь мы отключаем некоторые «магические» функции Spring Boot, которые нам не нужны для нашего конкретного теста -
@ComponentScan
: мы@ComponentScan
запуск приложения Spring Boot, сканируя только один пакет вместо всего проекта. Любой Spring-аннотированный класс, находящийся вне этого пакета, не будет выбран Spring.
Вот как выглядит интеграционный тест:
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
|
@RunWith (SpringJUnit4ClassRunner. class ) @WebIntegrationTest ( "server.port:0" ) @SpringApplicationConfiguration (classes = DelayedWebServerConfiguration. class ) @ActiveProfiles ( "restTemplateTimeout" ) public class RestTemplateShould { @Rule public ExpectedException thrown = none(); @Value ( "${local.server.port}" ) private int port; @Autowired private RestTemplate restTemplate; @Test public void throw_timeout_if_response_lasts_more_than_two_seconds() { thrown.expect(ResourceAccessException. class ); thrown.expectCause(instanceOf(SocketTimeoutException. class )); callEndpointWithDelay( 3000 ); } @Test public void do_not_throw_timeout_if_response_lasts_less_than_two_seconds() { callEndpointWithDelay( 10 ); } private void callEndpointWithDelay( long delayMs) { restTemplate.getForObject( } } |
Конечно, все эти классы хранятся в нашей папке исходного кода теста (обычно это src/test/java
), так как они не требуются для производства.
Давайте еще раз посмотрим на аннотации:
-
@RunWith
: в тесте будет использоваться раннер Spring Junit, который позаботится о создании для нас контекста Spring. -
@WebIntegrationTest
: сообщает Spring, что это интеграционный тест, запускающий веб-приложение, иначе Spring по умолчанию не будет запускать HTTP-сервер в тестовом режиме. Мы также устанавливаем дляserver.port
значение0
чтобы Spring Boot выбирал случайный порт для HTTP-сервера для прослушивания. Это позволяет запускать несколько тестов параллельно или запускать другую версию приложения в фоновом режиме. -
@SpringApplicationConfiguration
: мы@SpringApplicationConfiguration
Spring, где он найдет классDelayedWebServerConfiguration
мы создали ранее. -
@ActiveProfiles
: включает профильrestTemplateTimeout
, в противном случаеController
иConfiguration
будут отфильтрованы.
Теперь у нас есть интеграционный тест с ограниченным набором зависимостей вместо всего приложения. Что если мы хотим пойти дальше и добавить в игру насмешки? Это может потребоваться, если у зависимости нет среды разработки или она слишком сложна для вызова с рабочей станции разработчика. В этом случае мы можем добавить эти макеты в класс Configuration
и они будут внедрены в контекст Spring теста.
Вот пример Configuration
где мы внедряем пользовательскую службу CustomerService
, смоделированную Mockito, вместо стандартной:
01
02
03
04
05
06
07
08
09
10
11
12
|
@Profile ( "validationTests" ) @Configuration @EnableAutoConfiguration ( exclude = {DataSourceAutoConfiguration. class , HibernateJpaAutoConfiguration. class }) @ComponentScan (basePackages = { "my.application.controller" , "my.application.actions" }) public class ValidationEndToEndConfiguration { @Bean public CustomerService customerService() { return Mockito.mock(CustomerService. class ); } } |
При таком подходе мы можем сделать наши интеграционные тесты более устойчивыми. Для медленных или ненадежных зависимостей, более эффективно, чтобы разработчики запускали свои интеграционные тесты на макетированную версию. Однако не забывайте, что в конечном итоге ваше приложение должно будет интегрироваться с реальной системой, а не с поддельной. По этой причине имеет смысл, чтобы сервер непрерывной интеграции запускал тесты для реальной системы как минимум каждый день.
Ссылка: | Изолировать интеграционные тесты и макетные зависимости с помощью Spring Boot от нашего партнера по JCG Дэвида Хатаняна в блоге Crafted Software . |