Whether you follow a traditional test pyramid or a newer approach such as “test honeycomb,” you should start writing integration test cases at some point during development. You can write different types of integration tests. Starting with persistence tests, you can examine interactions between components or simulate calls to external services. This article discusses the latter case. Before we talk about WireMock, let’s start with a typical example.

ChuckNorrisService

We have a simple API for manual testing. The exception in the “business” class is that it can call external apis. It uses the Spring framework to provide functionality. Nothing special. What I see many times are tests that simulate the RestTemplate and return some pre-determined answer. The implementation might look something like this:

@Service public class ChuckNorrisService{ ... public ChuckNorrisFact retrieveFact() { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); }... }Copy the code

Next to regular unit tests that check for success, there will be at least one test case that overrides HTTP error codes, namely 4xx or 5XX status codes:

@Test
  public void shouldReturnBackupFactInCaseOfError() {
    String url = "http://localhost:8080";
    RestTemplate mockTemplate = mock(RestTemplate.class);
    ResponseEntity<ChuckNorrisFactResponse> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    when(mockTemplate.getForEntity(url, ChuckNorrisFactResponse.class)).thenReturn(responseEntity);
    ChuckNorrisService service = new ChuckNorrisService(mockTemplate, url);
    ChuckNorrisFact retrieved = service.retrieveFact();
    assertThat(retrieved).isEqualTo(ChuckNorrisService.BACKUP_FACT);
  }Copy the code

Doesn’t it look good? The response entity returns 503 error code and our service will not crash. All tests passed green and we were ready to deploy our application. Unfortunately, Spring’s RestTemplate cannot be used this way. The method signature getForEntity gives us a little hint. It indicates throws RestClientException. This is where the RestTemplate of the mock differs from the actual implementation. We will never receive ResponseEntity with a 4XX or 5XX status code. RestTemplate will throw a subclass of RestClientException. By looking at the hierarchy of classes, we can get a good idea of the results that might be thrown:

So let’s look at how to make this test better.

WireMock comes to the rescue

WireMock simulates the Web service by starting the mock server and returning the answer that it is configured to return. Thanks to the excellent DSL, it’s easy to integrate into your tests, and it’s easy to mock requests.

For JUnit 4, there is a WireMockRule tool that helps start and stop the server. For JUnit 5, you’ll probably need to build one yourself. When you examine the sample project, you can find ChuckNorrisServiceIntegrationTest. This is a SpringBoot test based on JUnit 4. Let’s see.

The most important part is the ClassRule:

@ClassRule
  public static WireMockRule wireMockRule = new WireMockRule();Copy the code

As mentioned earlier, this will start and stop the WireMock server. You can also use the Rule Rule as usual to start and stop each server under test. This is not required for our tests.

Next, you’ll see several configureWireMockFor… Methods. These contain instructions for when WireMock returns an answer. Separating the WireMock configuration into several methods and calling them from tests is how I used WireMock. Of course, you can set up all possible requests in a single @before method. For a properly used Demo, we do this:

public void configureWireMockForOkResponse(ChuckNorrisFact fact) throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", fact);
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))));
  }Copy the code

All the way from static imports com. Making. Tomakehurst. Wiremock. Client. Wiremock. As you can see, we store HTTP GET to the path /jokes/ Random and return a JSON object. The okJson() method is simply shorthand for a 200 response with JSON content. For error cases, the code is even simpler:

private void configureWireMockForErrorResponse() { stubFor(get(urlEqualTo("/jokes/random")) .willReturn(serverError()));  }Copy the code

As you can see, DSLS make reading instructions easy. With WireMock in place, we can see that our previous implementation doesn’t work because the RestTemplate throws an exception. Therefore, we must adjust the code:

public ChuckNorrisFact retrieveFact() { try { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); } catch (HttpStatusCodeException e){ return BACKUP_FACT; }}Copy the code

This covers the basic use cases for WireMock. Configure the answers to the requests, perform the tests, and check the results, so easy. However, there is usually a problem when running tests in a cloud environment. Let’s see what we can do.

WireMock on a dynamic port

You may have noticed that the project integration test contains a ApplicationContextInitializer classes, and its @ TestPropertySource annotation will cover the actual API URL. That’s because I want to start WireMock on a random port. Of course, you can configure a fixed port for WireMock and treat this port as a constant in your tests. However, if your tests are running on the infrastructure of some cloud provider, it is not possible to determine whether the port is available. Therefore, I think random ports are better.

However, when we use properties in our Spring application, we must somehow pass random ports to our service. Or, as you can see in the example, override the URL. That is why we use ApplicationContextInitializer. We add the dynamically allocated port to the application context and can then reference it with the property ${wiremock.port}. The only downside here is that we now have to use ClassRule. Otherwise, we cannot access the port before initializing the Spring application.

With that out of the way, let’s take a look at a common problem involving HTTP calls.

timeout

WireMock offers more response possibilities than just a simple reply to a GET request. Another test case that is often forgotten is a test timeout. Developers often forget to set timeout URLConnections in RestTemplate. If there is no timeout, both wait an unlimited amount of time to respond. At best, at worst, all threads will wait for a response that never arrives.

Therefore, we should add a test that simulates a timeout. Of course, we could also use Mockito simulations to create delays, but in that case, we’ll be guessing the behavior of the RestTemplate again. Using WireMock to simulate latency is very simple:

private void configureWireMockForSlowResponse() throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, ""));
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(
            okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))
                .withFixedDelay((int) Duration.ofSeconds(10L).toMillis())));
  }Copy the code

WithFixedDelay () expects an int value representing milliseconds. I prefer to use Duration or at least a constant that says this parameter represents milliseconds without having to look at code comments every time I write code.

After setting the timeout RestTemplate and adding the test for the response, we can see that the RestTemplate throws a ResourceAccessException. Therefore, we can adjust the catch block to catch this exception and HttpStatusCodeException or just superclasses of both:

public ChuckNorrisFact retrieveFact() { try { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); } catch (RestClientException e){ return BACKUP_FACT; }}Copy the code

Now that we have a good look at the most common situations when performing HTTP requests, we can be sure that we are testing conditions that are close to the real thing.

Why not?

Another option for HTTP integration testing is Hoverfly. It works similarly to WireMock, but I prefer the latter. The reason is that WireMock is also useful when running end-to-end tests that include a browser. Hoverfly (or at least the Java library) is limited by JVM agents. This might make it faster than WireMock, but when, for example, some JavaScript code starts to work, it doesn’t work at all. WireMock’s ability to start a Web server is useful when your browser code also calls other services directly. Then, you can also mock them using WireMock and write tests such as Selenium.

conclusion

This article shows you two things:

  • The importance of integration testing
  • WireMock is a great testing framework

Of course, a great deal could be written on both topics. However, I shared how to use WireMock and its capabilities. Read more of their documentation along the way and try out other features such as WireMock for authentication.

  • Solemnly declare: the article prohibits the third party (except Tencent cloud) reprint, published, the story of the test nest, home copy I seven original shielding, your conscience will not hurt?