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

# やること
KotlinでTodoを管理するAPIサーバーを実装し、テストを書く。

# きっかけ
趣味でサーバーサイドKotlin(ktor/exposed)を触っているのですが、Kotlinなテストコードを書いたことが無かったため1から実装してみました。
# 成果物
https://github.com/smizuno2018/kotlin-todo-server

# 参考にしたリポジトリ
[こちらのリポジトリ](https://github.com/raharrison/kotlin-ktor-exposed-starter)を参考にさせて頂きました。

# 使用したライブラリなど
- 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
サーバーの起動などなど。

# テーブル定義
```kotlin: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のメソッドを呼ぶ。

```kotlin: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メソッドでレコードを更新する。

```kotlin: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でレスポンスを返す

```kotlin: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更新処理)

```kotlin: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更新処理)

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