Рассмотрим класс обслуживания, отвечающий за выполнение удаленного вызова и получение сведений:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
... public class CitiesService { private final WebClient.Builder webClientBuilder; private final String baseUrl; public CitiesService( WebClient.Builder webClientBuilder, @Value ( "${cityservice.url}" ) String baseUrl) { this .webClientBuilder = webClientBuilder; this .baseUrl = baseUrl; } public Flux<City> getCities() { return this .webClientBuilder.build() .get() .... |
Это Spring Bean и разрешает URL для вызова через свойство cityservice.url.
Если я хотел протестировать этот класс, то подход, который я использовал при использовании WebClient, состоит в том, чтобы запустить фиктивный сервер, используя отличный Wiremock, и использовать его для тестирования этого класса. Макет Wiremock выглядит так:
01
02
03
04
05
06
07
08
09
10
11
12
|
private static final WireMockServer WIREMOCK_SERVER = new WireMockServer(wireMockConfig().dynamicPort()); ..... WIREMOCK_SERVER.stubFor(get(urlEqualTo( "/cities" )) .withHeader( "Accept" , equalTo( "application/json" )) .willReturn(aResponse() .withStatus( 200 ) .withHeader( "Content-Type" , "application/json" ) .withBody(resultJson))); |
Сервер Wiremock запускается через произвольный порт, и он настроен для ответа на конечную точку, называемую «/ города». Вот где возникает проблема курицы и яйца :
1. Класс CitiesService требует, чтобы свойство с именем cityservice.url было установлено перед началом теста.
2. Wiremock запускается через произвольный порт, и URL-адрес, на который он отвечает, — «http: // localhost: randomport» и доступен только после запуска теста.
Есть три возможных решения, которые я могу придумать, чтобы разорвать эту круговую зависимость:
Подход 1: использовать жестко закодированный порт
Этот подход зависит от запуска Wiremock на фиксированном порту вместо динамического порта, таким образом, свойство может быть установлено при запуске теста, что-то вроде этого:
1
2
3
4
5
6
|
@ExtendWith (SpringExtension. class ) @SpringBootTest (classes = CitiesServiceHardcodedPortTest.SpringConfig. class , public class CitiesServiceHardcodedPortTest { private static final WireMockServer WIREMOCK_SERVER = new WireMockServer(wireMockConfig().port( 9876 )); |
Здесь Wiremock запускается на порту 9876, а свойство при запуске устанавливается на «http: // localhost: 9876 /».
Это решает проблему, однако, это не является дружественным для CI-сервера, возможно, что порты конфликтуют во время выполнения, и это делает некорректный тест.
Подход 2: не использовать Spring для теста
Лучший подход состоит в том, чтобы не использовать свойство по следующим направлениям:
01
02
03
04
05
06
07
08
09
10
11
12
|
public class CitiesServiceDirectTest { private static final WireMockServer WIREMOCK_SERVER = new WireMockServer(wireMockConfig().dynamicPort()); private CitiesService citiesService; @BeforeEach public void beforeEachTest() { final WebClient.Builder webClientBuilder = WebClient.builder(); this .citiesService = new CitiesService(webClientBuilder, WIREMOCK_SERVER.baseUrl()); } |
Здесь служба создается путем явной установки baseUrl в конструкторе, что позволяет избежать необходимости устанавливать свойство перед тестом.
Подход 3: Инициализатор контекста приложения
ApplicationContextInitializer используется для программной инициализации контекста приложения 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
|
@ExtendWith (SpringExtension. class ) @SpringBootTest (classes = CitiesServiceSpringTest.SpringConfig. class ) @ContextConfiguration (initializers = {CitiesServiceSpringTest.PropertiesInitializer. class }) public class CitiesServiceSpringTest { private static final WireMockServer WIREMOCK_SERVER = new WireMockServer(wireMockConfig().dynamicPort()); @Autowired private CitiesService citiesService; @Test public void testGetCitiesCleanFlow() throws Exception { ... } static class PropertiesInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { TestPropertyValues.of( ).applyTo(applicationContext.getEnvironment()); } } } |
Сначала запускается Wiremock, затем инициализируется контекст Spring с помощью инициализатора, который внедряет свойство «cityservice.url» через динамический порт Wiremocks, таким образом, свойство доступно для подключения к CityService.
Вывод
Я лично предпочитаю Подход 2, однако хорошо, чтобы проводка Spring и зависимые bean-компоненты создавались перед тестом, и если класс использует их, то я предпочитаю Подход 3. Инициализатор контекста приложения предоставляет хороший способ решить проблему курицы и яйца. свойства, подобные этим, которые должны быть доступны до начала взаимодействия с Spring.
Все примеры кода доступны здесь:
Подход 1: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceHardcodedPortTest.java
Подход 2: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceDirectTest.java
Подход 3: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceSpringTest.java
Смотрите оригинальную статью здесь: Курица и яйцо — разрешение свойств Spring перед тестом Мнения, высказанные участниками Java Code Geeks, являются их собственными. |