19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

サーバーサイドKotlin(ktor/exposed)入門

Last updated at Posted at 2020-11-15

やること

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を作って遊んでみてください。

19
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?