Skip to content

flowable/flowable-mockwebserver

Repository files navigation

Flowable Mock Web Server

Latest Version License

Build Status

Web Server for testing HTTP clients

Motivation

This library is inspired by the OkHttp MockWebServer. It allows to specify which the responses that the server should return for each request and to verify the requests that the server received.

The reason for creating this library is that we wanted to have a similar functionality for testing HTTP clients in Java, but without the need to include the whole OkHttp library. We are using a light weight webserver Microhttp to handle the requests. The use of Microhttp limits the functionality of the server (i.e. there is no HTTPS / TLS support and no HTTP 2 support), but it is enough for our needs.

There is also a different library WireMock that provides similar functionality, but that one is even more complex (with more dependencies) and has more features than we needed.

Example

Here is a complete example

class ExampleTest {

    protected MockWebServer server;

    @BeforeEach
    void setUp() {
        server = new MockWebServer();
        server.start();
        server.failFast();
    }

    @AfterEach
    void tearDown() {
        server.shutdown();
    }

    @Test
    void queryPets() throws IOException, InterruptedException {
        HttpClient client = HttpClient.newHttpClient();
        server.enqueue(MockResponse.newBuilder().jsonBody("""
                [
                  {
                    "id": 1,
                    "name": "Loki",
                    "type": "dog"
                  },
                  {
                    "id": 2,
                    "name": "Garfield",
                    "type": "cat"
                  }
                ]
                """));
        server.enqueue(MockResponse.newBuilder().jsonBody("""
                {
                  "message": "Not Found"
                }
                """).notFound());

        String petsUrl = server.url("/pets");
        HttpResponse<String> response = client.send(HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(petsUrl))
                .build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.headers().firstValue("Content-Type"))
                .hasValue("application/json");
        assertThat(response.body())
                .isEqualTo("""
                        [
                          {
                            "id": 1,
                            "name": "Loki",
                            "type": "dog"
                          },
                          {
                            "id": 2,
                            "name": "Garfield",
                            "type": "cat"
                          }
                        ]
                        """);

        response = client.send(HttpRequest.newBuilder()
                .PUT(HttpRequest.BodyPublishers.ofString("""
                        {
                          "name": "Loki Updated",
                          "type": "dog"
                        }
                        """))
                .uri(URI.create(petsUrl + "/1"))
                .build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

        assertThat(response.statusCode()).isEqualTo(404);
        assertThat(response.body()).isEqualTo("""
                {
                  "message": "Not Found"
                }
                """);

        RecordedRequest recordedRequest = server.takeRequest();
        assertThat(recordedRequest.path()).isEqualTo("/pets");
        assertThat(recordedRequest.method()).isEqualTo("GET");
        assertThat(recordedRequest.requestUrl().queryParameters()).isEmpty();

        recordedRequest = server.takeRequest();
        assertThat(recordedRequest.path()).isEqualTo("/pets/1");
        assertThat(recordedRequest.method()).isEqualTo("PUT");
        assertThat(recordedRequest.requestUrl().queryParameters()).isEmpty();
        assertThat(recordedRequest.body().asString())
                .isEqualTo("""
                        {
                          "name": "Loki Updated",
                          "type": "dog"
                        }
                        """);
    }
}

API

MockResponse

MockResponse is used to specify the response that the server should return for the next request. By default, an empty response with status code 200 is returned. They are created using MockResponseBuilder and you can customize it using a fluent API.

MockResponse response = MockResponse.newBuilder()
        .jsonBody("""
                {
                  "id": 1,
                  "name": "Loki",
                  "type": "dog"
                }
                """)
        .header("Cache-Control", "no-cache")
        .build();

Responses can also be throttled, to simulate a slow network.

MockResponse response = MockResponse.newBuilder()
        .jsonBody("{}")
        .bodyDelay(Duration.ofSeconds(2))
        .build();

The status of the response can be configured via MockResponse.status / MockResponse.statusCode. It can directly be set using an integer value of the status or through some of the known statuses in MockHttpStatus

MockWebServer

The main entry point where response can be queued and requests can be retrieved to be verified. It can be started on a random port (using start()) or on a specific port (start(31313)). The URL of a specific path on the server can be retrieved using url("/path").

MockWebServer server = new MockWebServer();
server.start();
int port = server.getPort();
String petsUrl = server.url("/pets");

Responses can be enqueued directly using the MockResponseBuilder or MockResponse

e.g.

server.enqueue(MockResponse.newBuilder()
    .jsonBody("""
        {
          "message": "Not Found"
        }
        """)
    .notFound());
MockResponse response = MockResponse.newBuilder()
    .jsonBody("""
        {
          "message": "Not Found"
        }
        """)
    .notFound()
    .build();
server.enqueue(response);

Once you are done with the server it can be shutdown via server.shutdown().

It is also possible to provide a custom (non queue based) response provider.

Function<RecordedRequest, MockResponse> customResponseProvider = request -> switch (request.path()) {
    case "/login/auth/":
        return MockResponse.newBuilder().build();
    case "/check/version/":
        return MockResponse.newBuilder().body("version=9");
    case "/pets/1":
        return MockResponse.newBuilder().jsonBody("""
                {
                  "id": 1,
                  "name": "Loki",
                  "type": "dog"
                }
                """);
    default:
        return MockResponse.newBuilder().notFound().build();
};

RecordedRequest

RecordedRequest is used to verify the requests that the server received. They can be retrieved using server.takeRequest() (which returns immediately) or server.takeRequest(Duration.ofSeconds(5)) (which will wait 5 seconds for a request to be available before returning null).

RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.path()).isEqualTo("/pets/1");
assertThat(recordedRequest.method()).isEqualTo("PUT");
assertThat(recordedRequest.header("Content-Type")).isEqualTo("application/json");
assertThat(recordedRequest.headerValues("Content-Type")).containsExactly("application/json");
assertThat(recordedRequest.body().asString()).isEqualTo("""
        {
          "name": "Loki Updated",
          "type": "dog"
        }
        """);