1. 概述
本文我们学习如何为 Ktor 控制器编写测试。我们会创建一个用于测试的 Ktor API,不涉及数据库,专注于测试逻辑本身。
2. 项目搭建
首先在 build.gradle
文件中添加 Ktor 的核心依赖:
implementation("io.ktor", "ktor-server-core", "2.3.11")
implementation("io.ktor", "ktor-server-netty", "2.3.11")
implementation("io.ktor", "ktor-serialization-jackson", "2.3.11")
然后添加测试所需的依赖:
testImplementation("io.ktor", "ktor-server-tests", "2.3.11")
testImplementation("org.jetbrains.kotlin", "kotlin-test-junit", "1.9.10")
2.1. 内容协商配置
我们使用 Jackson 实现内容序列化。创建一个 Application
的扩展方法:
fun Application.configureContentNegotiation() {
install(ContentNegotiation) {
jackson()
}
}
2.2. 路由配置
为应用添加路由配置:
fun Application.configureRouting() {
routing {
route("cars") { }
}
}
2.3. 嵌入式服务器启动
启动 Ktor 嵌入式服务器:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
configureRouting()
configureContentNegotiation()
}.start(wait = true)
}
3. Ktor 控制器实现
定义 Car
类作为领域模型:
data class Car(
val id: String,
var brand: String,
var price: Double
)
使用一个模拟的存储类代替数据库:
object CarStorageMock {
val carStorage = ArrayList<Car>()
}
在路由中实现 CRUD 操作:
get {
call.respond(CarStorageMock.carStorage)
}
get("{id?}") {
val id = call.parameters["id"]
val car = CarStorageMock.carStorage.find { it.id == id } ?: return@get call.respondText(
text = "car.not.found",
status = HttpStatusCode.NotFound
)
call.respond(car)
}
post {
val car = call.receive<Car>()
CarStorageMock.carStorage.add(car)
call.respond(status = HttpStatusCode.Created, message = car)
}
put("{id?}") {
val id = call.parameters["id"]
val car = CarStorageMock.carStorage.find { it.id == id } ?: return@put call.respondText(
text = "car.not.found",
status = HttpStatusCode.NotFound
)
val carUpdate = call.receive<Car>()
car.brand = carUpdate.brand
car.price = carUpdate.price
call.respond(car)
}
delete("{id?}") {
val id = call.parameters["id"]
if (CarStorageMock.carStorage.removeIf { it.id == id }) {
call.respondText(text = "car.deleted", status = HttpStatusCode.OK)
} else {
call.respondText(text = "car.not.found", status = HttpStatusCode.NotFound)
}
}
4. 测试环境搭建
我们使用 testApplication()
方法进行测试,并手动配置模块。
创建一个通用方法来初始化客户端:
private fun ApplicationTestBuilder.configureServerAndGetClient(): HttpClient {
application {
configureRouting()
configureContentNegotiation()
}
val client = createClient {
install(ContentNegotiation) {
jackson()
}
}
return client
}
每次测试前清空模拟数据:
@Before
fun before() {
CarStorageMock.carStorage.clear()
}
4.1. GET 请求测试
获取所有车辆列表
CarStorageMock.carStorage.addAll(
listOf(
Car(id = "1", brand = "BMW", price = 10_000.0),
Car(id = "2", brand = "Audi", price = 11_000.0)
)
)
val response = client.get("/cars")
val responseBody: List<Car> = response.body()
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(2, responseBody.size)
val bmwCar = responseBody.find { it.id == "1" }
assertEquals("BMW", bmwCar?.brand)
assertEquals(10_000.0, bmwCar?.price)
根据 ID 获取车辆
val response = client.get("/cars/1")
val responseBody: Car = response.body()
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("1", responseBody.id)
assertEquals("BMW", responseBody.brand)
获取不存在的 ID
val response = client.get("/cars/3")
val responseText = response.bodyAsText()
assertEquals(HttpStatusCode.NotFound, response.status)
assertEquals("car.not.found", responseText)
4.2. POST 请求测试
val response = client.post("/cars") {
contentType(ContentType.Application.Json)
setBody(Car(id = "2", brand = "Audi", price = 11_000.0))
}
val responseBody: Car = response.body()
assertEquals(HttpStatusCode.Created, response.status)
assertEquals("2", responseBody.id)
assertEquals(1, CarStorageMock.carStorage.size)
4.3. PUT 请求测试
正常更新
CarStorageMock.carStorage.add(Car(id = "1", brand = "BMW", price = 10_000.0))
val response = client.put("/cars/1") {
contentType(ContentType.Application.Json)
setBody(Car(id = "1", brand = "Audi", price = 11_000.0))
}
val responseBody: Car = response.body()
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Audi", responseBody.brand)
更新不存在的 ID
val response = client.put("/cars/2") {
contentType(ContentType.Application.Json)
setBody(Car(id = "1", brand = "Audi", price = 11_000.0))
}
val responseText = response.bodyAsText()
assertEquals(HttpStatusCode.NotFound, response.status)
assertEquals("car.not.found", responseText)
4.4. DELETE 请求测试
删除存在的车辆
CarStorageMock.carStorage.add(Car(id = "1", brand = "BMW", price = 10_000.0))
val response = client.delete("/cars/1")
val responseText = response.bodyAsText()
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("car.deleted", responseText)
删除不存在的车辆
val response = client.delete("/cars/2")
val responseText = response.bodyAsText()
assertEquals(HttpStatusCode.NotFound, response.status)
assertEquals("car.not.found", responseText)
5. 总结
本文我们搭建了一个简单的 Ktor 服务并为其控制器编写了完整的测试用例。即使将来引入数据库,测试逻辑依然有效,只需替换 CarStorageMock
的实现即可。
所有代码可在 GitHub 上找到。✅