やること
KotlinでTodoを管理するAPIサーバーを実装し、テストを書く。
きっかけ
趣味でサーバーサイドKotlin(ktor/exposed)を触っているのですが、Kotlinなテストコードを書いたことが無かったため1から実装してみました。
成果物
参考にしたリポジトリ
こちらのリポジトリを参考にさせて頂きました。
使用したライブラリなど
- ktor
- exposed
- ktor-jackson
- postgresql
- assertj-core
- rest-assured
- junit-jupiter-api
- junit-jupiter-engine
...
ディレクトリ構成
├── docker-compose.yml
├── src
│ ├── Application.kt
│ ├── model
│ │ ├── BaseResponse.kt
│ │ ├── ErrorResponse.kt
│ │ ├── Exception.kt
│ │ └── Todos.kt
│ ├── service
│ │ ├── DatabaseFactory.kt
│ │ └── TodoService.kt
│ └── web
│ └── TodoResource.kt
└── test
├── common
│ └── ServerTest.kt
├── service
│ └── TodoServiceTest.kt
└── web
└── TodoResourceTest.kt
各役割
TodoService
DBへクエリを実行。
TodoResource
ルーティング。TodoServiceからの実行結果、例外を受け取りAPIのResponseとして返す。
Application
サーバーの起動などなど。
テーブル定義
Todos.kt
object Todos : Table() {
val id = long("id").autoIncrement().check { it greaterEq TodosConstant.ID_START }
val title = varchar("title", TodosConstant.TITLE_MAX_LENGTH) // 100文字制限
val detail = varchar("detail", TodosConstant.DETAIL_MAX_LENGTH).nullable() // 1000文字制限
val date = date("date").nullable()
private val createdAt = datetime("created_at").defaultExpression(CurrentDateTime())
private val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime())
override val primaryKey = PrimaryKey(id)
}
処理の流れ(Todo更新処理)
1.TodoResourceでPUTリクエストを受け取り、TodoServiceのメソッドを呼ぶ。
TodoResource.kt
route("/todos") {
put("/{id}") {
try {
val id = call.parameters["id"]!!.toLong() // Todoを更新する対象のidを変数へ入れる。
val newTodo = call.receive<NewTodo>() // リクエストパラメータをdata classへ入れる。
todoService.updateTodo(id, newTodo)
call.respond(OK, TodoResponse())
} catch (e: Exception) {
...
2.updateメソッドでレコードを更新する。
TodoService.kt
fun updateTodo(id: Long, todo: NewTodo) {
val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
try {
transaction {
val isFailed = Todos.update({ Todos.id eq id }) {
it[title] = todo.title
it[detail] = todo.detail
it[date] = DateTime.parse(todo.date, formatter)
} == 0
if (isFailed) throw RecordInvalidException()
}
} catch (e: Throwable) {
...
3.TodoServiceで例外が起きなければ、status200でレスポンスを返す
TodoResource.kt
put("/{id}") {
try {
val id = call.parameters["id"]!!.toLong()
val newTodo = call.receive<NewTodo>()
todoService.updateTodo(id, newTodo)
call.respond(OK, TodoResponse())
} catch (e: Exception) {
when (e) {
is NullPointerException, is JsonParseException -> {
throw BadRequestException()
}
is RecordInvalidException -> {
throw InternalServerErrorException(PUT_ERROR_MESSAGE, PUT_ERROR_CODE)
}
else -> throw UnknownException()
}
}
}
成功系テストコード(Todo更新処理)
TodoResourceTest.kt
@Test
fun testPutTodo() {
// given
val oldTodo = NewTodo("title", "detail", "2020-01-01")
post(oldTodo)
// when
val newTodo = NewTodo("title2", "detail2", "2020-02-02")
val response = given()
.contentType(ContentType.JSON)
.body(newTodo)
.When()
.put("/todos/{id}", ID_START)
.then()
.statusCode(200)
.extract().to<TodoResponse>()
// then
assertThat(response.errorCode).isEqualTo(0)
assertThat(response.errorMessage).isEqualTo("")
}
private fun post(todo: NewTodo): TodoResponse {
return given()
.contentType(ContentType.JSON)
.body(todo)
.When()
.post("/todos")
.then()
.statusCode(200)
.extract().to()
}
失敗系テストコード(Todo更新処理)
TodoResourceTest.kt
fun `Todo更新時にリクエストパラメータが不正の場合、エラーレスポンスを返す`() {
// when
val response = given()
.contentType(ContentType.JSON)
.body("Illegal parameter")
.When()
.put("/todos/{id}", ID_START)
.then()
.statusCode(400)
.extract().to<TodoResponse>()
// then
assertThat(response.errorCode).isEqualTo(2)
assertThat(response.errorMessage).isEqualTo("リクエストの形式が不正です")
}
終わりに
是非、より良い書き方、ライブラリございましたら、ご指摘いただきますと幸いです。
サーバーサイドKotlinに興味がある方は、ktor/exposedでTodo APIを作って遊んでみてください。