... 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() ....
This is a Spring Bean and resolves the url to call through a property called "cityservice.url".
If I wanted to test this class, an approach that I have been using when using WebClient is to start a mock server using the excellent Wiremock and using it to test this class. A Wiremock mock looks like this:
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)));
The Wiremock server is being started up at a random port and it is set to respond to an endpoint called "/cities". Here is where the chicken and egg problem comes up:
1. The CitiesService class requires a property called "cityservice.url" to be set before starting the test.
2. Wiremock is started at a random port and the url that it is responding to is "http://localhost:randomport" and is available only once the test is kicked off.
There are three potential solutions that I can think of to break this circular dependency:
Approach 1: To use a hardcoded port
This approach depends on starting up Wiremock on a fixed port instead of a dynamic port, this way the property can be set when starting the test up, something like this:@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));
Here Wiremock is being started at port 9876 and the property at startup is being set to "http://localhost:9876/".
This solves the problem, however, this is not CI server friendly, it is possible for the ports to collide at runtime and this makes for a flaky test.
Approach 2: Not use Spring for test
A better approach is to not use the property, along these lines: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()); }
Here the service is being created by explicitly setting the baseUrl in the constructor, thus avoiding the need to set a property ahead of the test.
Approach 3: Application Context Initializer
ApplicationContextInitializer is used for programmatically initializing a Spring Application Context and it can be used with a test to inject in the property before the actual test is executed. Along these lines:
@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 is started up first, then Spring context is initialized using the initializer which injects in the "cityservice.url" property using the Wiremocks dynamic port, this way the property is available for wiring into CityService.
Conclusion
I personally prefer Approach 2, however it is good to have Spring's wiring and the dependent beans created ahead of the test and if the class utilizes these then I prefer Approach 3. Application Context initializer provides a good way to break the chicken and egg problem with properties like these which need to be available ahead of Spring's context getting engaged.All the code samples are available here:
Approach 1: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceHardcodedPortTest.java
Approach 2: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceDirectTest.java
Approach 3: https://github.com/bijukunjummen/reactive-cities-demo/blob/master/src/test/java/samples/geo/service/CitiesServiceSpringTest.java
No comments:
Post a Comment