membersound
membersound

Reputation: 86925

How to set MockWebServer port to WebClient in JUnit test?

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());
        }
    }
}

Upvotes: 24

Views: 19911

Answers (4)

Laxmikant Mahamuni
Laxmikant Mahamuni

Reputation: 131

I was searching for "How to set explicit port to MockWebServer to avoid default port number"

Above hint worked for me.. i.e. mockWebServer.start(port);

I used Kotlin so my code became like below:

var mockWebServer: MockWebServer = MockWebServer().apply {
        Logger.verbose("In MockHttpApiClient: apply{}")
        this.dispatcher = createDispatcher()
        start(8080)
    }

Some one may get hint from this.

Upvotes: 1

Joe Conti
Joe Conti

Reputation: 31

Here's a solution that's working from me. Spring Boot 2 & Spring-Webflux 5.3.30 using SpringBootTest

  1. MyServiceClientTestConfig creates injected beans port and myServiceClient, where

    • The port Integer bean is generated using Spring's TestSocketUtils.findAvailableTcpPort()
    • MyServiceClient is a Spring service that uses WebClient and gets initialized with a base URL.
  2. As documented we should not re-use a MockWebServer instance between tests so create it before each test using the port we found free at test initialization and shut it down after each test.

I suppose there is a slim chance that since I find the port once when Spring is initializing the application context and there are short periods of time where it is not in use by MockWebServer, before the first call to init and between tests, that another process could take the port but the odds of that happening during the lifetime of the test seem incredibly small.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MyServiceClientTest {

    private static final Logger LOG = LoggerFactory.getLogger(MyServiceClientTest.class);

    private MockWebServer mockWebServer;

    @Autowired
    private MyServiceClient myServiceClient;

    @Autowired
    private Integer port;

    @BeforeEach
    void init() throws IOException {
        assertNotNull(myServiceClient);
        assertNotNull(port);

        // MockWebServer not re-usable between tests
        mockWebServer = new MockWebServer();
        mockWebServer.start(port);

        assertNotNull(mockWebServer);
        assertEquals(port, mockWebServer.getPort());
    }

    @AfterEach
    void cleanUp() throws IOException {
        // MockWebServer not re-usable between tests
        mockWebServer.shutdown();
    }

    @Test
    public void placeholderTest1() {
        LOG.info("Hello 1 {}", mockWebServer.getPort());
    }

    @Test
    public void placeholderTest2() {
        LOG.info("Hello 2 {}", mockWebServer.getPort());
    }

    @Configuration
    static class MyServiceClientTestConfig {

        @Bean
        Integer getFreePort () {
            // find a free port to use for this test
            return TestSocketUtils.findAvailableTcpPort();
        }

        @Bean
        MyServiceClient getMyServiceClient(Integer port) {
            return new MyServiceClient(String.format("http://localhost:%d", port));
        }
    }
}

Upvotes: 3

Thomas Turrell-Croft
Thomas Turrell-Croft

Reputation: 742

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!"));
  }
}

Upvotes: 1

magiccrafter
magiccrafter

Reputation: 5482

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();
    }
}

Upvotes: 23

Related Questions