1. 简介

在软件开发中,测试是确保我们应用程序质量和可靠性的重要手段。当测试的应用需要通过 API 与外部服务交互时,在测试过程中直接调用真实接口往往是不现实且不可靠的。这时 MockServer 就派上用场了!

MockServer 是一个强大的工具,它可以模拟 API 接口和响应,使我们能够在隔离环境中测试应用逻辑,非常适合用于集成测试或单元测试。在这篇文章中,我们将结合 Kotest —— Kotlin 的一个流行测试框架,演示如何使用 MockServer 编写更健壮、更全面的测试。

通过本文,你将学会:

  • 启动和管理 MockServer 实例
  • 定义对请求的预期
  • 使用 Kotest 的断言能力验证请求和响应

2. 添加 MockServer 扩展依赖

首先,我们需要在项目中添加 MockServer Kotest 扩展。这个扩展简化了 MockServer 的集成流程。

如果你使用 Maven,添加如下依赖到 pom.xml

<dependency>
    <groupId>io.kotest.extensions</groupId>
    <artifactId>kotest-extensions-mockserver</artifactId>
    <version>1.2.1</version>
    <scope>test</scope>
</dependency>

添加完成后,我们就可以在测试类中使用 MockServer 了。接下来我们通过 Kotest 的 listener() 方法添加 MockServerListener

class MockServerTest : FunSpec({
    val mockServerListener = MockServerListener(1080)
    listener(mockServerListener)
    // ...
})

说明: 通过添加这个 listener,Kotest 会自动管理 MockServer 的生命周期。测试运行时,MockServer 将监听在 1080 端口上,供测试使用。

3. 准备一个 Mock 请求

我们可以在 beforeTest 生命周期钩子中定义一个简单的 /login 接口模拟:

class MockServerTest : FunSpec({
    val mockServerListener = MockServerListener(1080)
    listener(mockServerListener)

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

    afterTest {
        mockServerListener.close()
    }
})

说明: 上述代码在 beforeTest 中配置了一个期望的请求,并返回 202 Accepted 状态码。afterTest 用于确保测试结束后关闭 MockServer。

4. 编写测试用例

接下来我们使用 Apache HttpClient 发起一个 POST 请求并进行断言:

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
}

说明: 这里我们使用 Apache HttpClient 发送请求,并通过 use() 确保资源正确释放(类似 Java 的 try-with-resources)。

5. 验证请求内容

除了验证响应,我们有时还需要验证客户端发送的请求是否符合预期。MockServer 提供了记录请求的能力,我们可以使用 retrieveRecordedRequests() 来获取所有记录的请求并进行断言。

下面是一个示例:

class UnspecifiedHttpRequestTest : FunSpec({
    val mockServerListener = MockServerListener(1080)
    listener(mockServerListener)

    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.replace("\r\n", "\n") shouldBe """{
            |  "username" : "foo",
            |  "password" : "bar"
            |}""".trimMargin()
        request.path.value shouldBe "/login"
    }

    afterTest {
        mockServerListener.close()
    }
})

说明:

  • 我们没有在 beforeTest 中指定具体的请求格式,而是接受任意请求
  • 使用 retrieveRecordedRequests(null) 获取所有记录的请求
  • 然后对请求头、路径和请求体进行断言

⚠️ 注意: 请求体中可能包含换行符 \r\n,我们使用 replace("\r\n", "\n") 来统一格式,避免断言失败。

6. 总结

MockServer 结合 Kotest 能够提供一个强大、高效的测试环境,尤其适用于需要与外部 API 交互的场景。通过模拟接口响应,我们可以:

  • 避免依赖真实服务
  • 控制测试环境
  • 提高测试覆盖率和稳定性

使用 MockServer 的 Kotest 扩展可以简化集成过程,使我们能够专注于测试逻辑的编写。

完整代码示例已上传至 GitHub。欢迎参考学习。


关键词总结:

  • MockServer
  • Kotest
  • 接口模拟
  • 请求验证
  • 测试自动化

适用场景:

  • 需要与第三方 API 交互的项目
  • 希望隔离外部依赖的单元测试
  • 需要模拟特定响应的集成测试

如果你正在使用 Kotest 并需要测试网络请求逻辑,MockServer 是一个非常值得尝试的工具!


原始标题:How to Use MockServer in a Kotest Test