Статьи

Цыпленок и яйцо — разрешение весенних свойств перед тестом

Рассмотрим класс обслуживания, отвечающий за выполнение удаленного вызова и получение сведений:

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,
        properties = "cityservice.url=http://localhost:9876")
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(
                    "cityservice.url=" + "http://localhost:" + WIREMOCK_SERVER.port()
            ).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, являются их собственными.