1. Introduction

In the world of software development, testing is an essential part of ensuring the quality and reliability of our applications. When it comes to testing applications that interact with external services through APIs, using real endpoints during testing can be impractical and unreliable. This is where MockServer comes to the rescue! MockServer is a powerful tool that allows us to simulate API endpoints and responses, making it an invaluable asset for testing our applications in isolation.

In this tutorial, we’ll explore how to leverage MockServer in conjunction with Kotest, a popular testing framework for Kotlin, to write robust and comprehensive tests. By the end of the tutorial, we’ll be able to create MockServer instances, define expectations for incoming requests, and verify interactions using Kotest’s expressive testing capabilities.

2. Adding the MockServer Extension

To get started, let’s add the MockServer Kotest extension to our project. This extension for Kotest simplifies the integration process and makes it even easier to work with MockServer. Let’s start by adding it to our build.gradle.kts:

dependencies {
    // Other dependencies...
    testImplementation("io.kotest.extensions:kotest-extensions-mockserver:1.2.1")
}

Now that we’ve added the MockServer Kotest extension to our project, it’s time to set up MockServer for testing. Let’s start by including MockServerListener() in our test by invoking listener():

class MockServerTest : FunSpec({

    listener(MockServerListener(1080))

})

By adding this listener, Kotest will control MockServer’s lifecycle. When we run our tests, a MockServer will be available at port 1080 for our tests to use. The listener() function is from Kotest core, and MockServerListener() is a listener implementation that comes from the extension.

3. Preparing a Request

Let’s use a beforeTest() lifecycle hook to prepare a simple mocked /login endpoint:

class MockServerTest : FunSpec({
    listener(MockServerListener(1080))

    beforeTest {
        MockServerClient("localhost", 1080).´when´(
            HttpRequest.request()
                  .withMethod("POST")
                  .withPath("/login")
                  .withHeader("Content-Type", "application/json")
                  .withBody("""{"username": "foo", "password": "bar"}""")
            ).respond(
                HttpResponse.response().withStatusCode(202)
            )
    }
})

With this code inside beforeTest(), we’ve created our MockServer and prepared an expected request, to which we’ll respond 202 Accepted. By setting this up, we’re now ready to perform requests and assertions with the mocked API.

4. Writing a Test Case

After setting MockServer up, let’s demonstrate a call and our expectations by using Apache HttpClient:

class MockServerTest : FunSpec({
    listener(MockServerListener(1080))

    beforeTest {
        MockServerClient("localhost", 1080).`when`(
            HttpRequest.request()
                .withMethod("POST")
                .withPath("/login")
                .withHeader("Content-Type", "application/json")
                .withBody("""{"username": "foo", "password": "bar"}""")
        ).respond(HttpResponse.response().withStatusCode(202))
    }

    test("Should return 202 status code") {
        val httpPost = HttpPost("http://localhost:1080/login").apply {
            entity = StringEntity("""{"username": "foo", "password": "bar"}""")
            setHeader("Content-Type", "application/json")
        }

        val response = HttpClients.createDefault().use { it.execute(httpPost) }
        val statusCode = response.statusLine.statusCode
        statusCode shouldBe 202
    }
})

In this test case, we’ve created an HttpPost to the MockServer that we configured to listen at http://localhost:1080. After creating a new request with HttpClients.createDefault(), we can send the request with execute(). Because these requests must be closed to free up resources, we enclose the request in a try-with-resources provided by use().

5. Verifying the HttpRequest

In our test case, we made assertions on what was returned as the MockServer response. However, there are situations where we may want to verify the request sent by our code. This can be particularly useful when we set up MockServer to respond differently based on various requests. This helps us to ensure that the correct request was made.

Let’s create a test where we do not specify the exact kind of request we expect beforehand, and then, we’ll perform assertions on it:

class UnspecifiedHttpRequestTest : FunSpec({
    listener(MockServerListener(1080))

    val client = MockServerClient("localhost", 1080)

    beforeTest {
        client.`when`(
            HttpRequest.request()
        ).respond(
            HttpResponse.response().withStatusCode(202)
        )
    }

    test("Should make a post with correct content") {
        val httpPost = HttpPost("http://localhost:1080/login").apply {
            entity = StringEntity("""{"username": "foo", "password": "bar"}""")
            setHeader("Content-Type", "application/json")
        }

        HttpClients.createDefault().use { it.execute(httpPost) }

        val request = client.retrieveRecordedRequests(null).first()

        request.getHeader("Content-Type") shouldContain "application/json"
        request.bodyAsJsonOrXmlString shouldBe """{
            |  "username" : "foo",
            |  "password" : "bar"
            |}""".trimMargin()
        request.path.value shouldBe "/login"
    }
})

In this test case, we’ve created an HttpPost request, similar to the previous example. However, the difference lies in how we’ve configured our MockServerClient to handle requests. Instead of specifying the exact type of request MockServer should expect, we’ve set it up to accept any HttpRequest.

To validate the request information, we utilize the retrieveRecordedRequests() function of our client. Passing null as an argument to retrieveRecordedRequests() allows us to retrieve all requests that were made during the test execution. This approach enables us to query the requests and subsequently perform assertions using Kotest’s shouldContain() and shouldBe() functions.

By employing this technique, we can ensure that the correct request was made without explicitly specifying all the details beforehand. This grants us flexibility and ease of validation while testing, as we can dynamically verify the recorded requests for our HttpPost operation.

6. Conclusion

MockServer, when used in conjunction with Kotest, proves to be a powerful and efficient tool for testing applications that interact with external services through APIs. By simulating API endpoints and responses, MockServer allows developers to create isolated and reliable test environments. Through the integration of the MockServer Kotest extension, setting up and controlling MockServer within our tests becomes seamless.

Throughout this article, we’ve demonstrated how to leverage MockServer to prepare and define expectations for incoming requests. By employing Kotest’s expressive testing capabilities, we’re able to perform requests and assertions with the mocked API effortlessly. This approach not only ensures the robustness of our applications but also facilitates a smoother testing process by eliminating the need for real endpoints during testing.

By mastering the use of MockServer and Kotest, developers can confidently write comprehensive tests that guarantee the quality and reliability of their software applications, ultimately leading to a more efficient and successful software development process.

As always, the code used in this article is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.