22

I'm using spring-boot with WebClient, which is autowired as a bean.

Problem: when writing a junit integration test, I have to use okhttp MockWebServer. This mock always starts up on a random port, eg localhost:14321.

Now my WebClient of course has a fixed url that it sends the requests to. This url may be given by an application.properties parameter like webclient.url=https://my.domain.com/, so I could override that field in a junit test. But only statically.

Question: how can I reset the WebClient bean inside a @SpringBootTest so that it sends the requests always to my mock server?

@Service
public class WebClientService {
     public WebClientService(WebClient.Builder builder, @Value("${webclient.url}" String url) {
          this.webClient = builder.baseUrl(url)...build();
     }

     public Response send() {
          return webClient.body().exchange().bodyToMono();
     }
}

@Service
public void CallingService {
      @Autowired
      private WebClientService service;

      public void call() {
           service.send();
      }
}


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyWebTest {
        @Autowired
        private CallingService calling;

        @Test
        public void test() {
             MockWebServer mockWebServer = new MockWebServer();
             System.out.println("Current mock server url: " + mockWebServer.url("/").toString()); //this is random    

             mockWebServer.enqueue(new MockResponse()....);

             //TODO how to make the mocked server url public to the WebClient?
             calling.call();
        }
}

As you see, I'm writing a full realworld junit integration test. The only problem is: how can I pass the MockWebServer url and port to the WebClient so that it automatically sends the requests to my mock??

Sidenote: I definitely need a random port in MockWebServer here to not interfer with other running tests or applications. Thus have to stick to the random port, and find a way to pass it to the webclient (or dynamically override the application property).


Update: I came up with the following, which works. But maybe anyone knows how to make the mockserver field non-static?

@ContextConfiguration(initializers = RandomPortInitializer.class)
public abstract class AbstractITest {
    @ClassRule
    public static final MockWebServer mockWebServer = new MockWebServer();

    public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
                    "webclient.url=" + mockWebServer.url("/").toString());
        }
    }
}
membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • Hello! Why are you using `MockWebServer` if you want to do an Integration Test? Isn't `SpringBootTest.WebEnvironment.RANDOM_PORT` enough for creating a complete web environment (without needing to mock anything)? – Gustavo Passini Jul 24 '19 at 16:20
  • 1
    I need to mock the *remote* webserver that is requested within my application. Of course I cannot execute itests against a live remote webserver. So that is to be mocked. As written, I'm creating a *client* application, not a server! – membersound Jul 24 '19 at 19:06
  • @membersound Have you tried my solution ? – Arnaud Claudel Jul 25 '19 at 07:48
  • As written, I have to mock the **remote** connection that the `WebClient` sends out of my application. This has nothing to do with the `RANDOM_PORT` property that spring starts my own webserver with! – membersound Jul 25 '19 at 07:52
  • But you said that your remote server also has a random port. By the way, why can't you try a fixed port ? It doesn't matter for the client but for the server, I think that you can easily find a free and stable port – Arnaud Claudel Jul 25 '19 at 08:47
  • Sorry then, I did not mean that, the remote server port is always fixed. – membersound Jul 25 '19 at 09:09
  • If it's fixed, why don't you use MockWebServer.start(PORT) ? – Arnaud Claudel Jul 25 '19 at 10:15
  • 1
    Imagine my remote webserver is accessed with `webclient.url=my.remote.domain:2233`. Now in my integration test I'd have to replace it with `webclient.url=localhost:`. Of course I could start the `MockWebServer` on port `2233`, but the next developer who's running the code might have another service running on 2233, so the rest fails as it cannot start the mock server. Or another example, if two applications are running integration test suite side by side, and both by change operating on port 2233. So I'd prefer injecting the random port into the `WebClient` to be 100% flexible. – membersound Jul 25 '19 at 10:26
  • regarding making mockWebServer non-static, I am not sure if it is possible because application context is loaded for a test class and then reused for each test in order to reduce the setup costs per test suite. We need to inject early those properties into the environment before the context is initialized for a test suite. – Alex Mar 26 '21 at 12:47

2 Answers2

21

Since Spring Framework 5.2.5 (Spring Boot 2.x) you can use DynamicPropertySource annotation which is quite handy.

Here is a complete example how you can use it with MockWebServer to bind the correct port:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public abstract class AbstractIT {

    static MockWebServer mockWebServer;

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry r) throws IOException {
        r.add("some-service.url", () -> "http://localhost:" + mockWebServer.getPort());
    }

    @BeforeAll
    static void beforeAll() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }

    @AfterAll
    static void afterAll() throws IOException {
        mockWebServer.shutdown();
    }
}
magiccrafter
  • 5,175
  • 1
  • 56
  • 50
  • 1
    DynamicPropertySource will result in instantiation of a new Spring context for each test which can have unexpected side effects like running out of DB connections – Justin Rowe Jun 16 '21 at 14:20
1

This is how you can set a WebClient base URL with the URL of the MockWebServer.

The WebClient and MockWebServer are light enough to be recreated for each test.

@SpringBootTest
class Tests {

  @Autowired
  private WebClient.Builder webClientBuilder;

  private MockWebServer mockWebServer;

  private WebClient client;

  @BeforeEach
  void setUp() throws Exception {
    mockWebServer = new MockWebServer();
    mockWebServer.start();

    // Set WebClinet base URL with the the mock web server URL 
    webClientBuilder.baseUrl(mockWebServer.url("").toString());

    client = webClientBuilder.build();

  }

  @AfterEach
  void tearDown() throws Exception {
    mockWebServer.shutdown();
  }

  @Test
  void bodyIsExpected() throws InterruptedException {

    mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK").setBody("Hello World!")
        .addHeader("Content-Type", "text/plain"));

    ResponseEntity<String> response = client.get().retrieve().toEntity(String.class).block();

    assertThat(response.getBody(), is("Hello World!"));
  }
}