kdnakt blog

hello there.

Kotlin/NativeでHTTPサーバーを作るアドベントカレンダー(13日目:例外をテストする)

この記事はkdnaktの1人 Advent Calendar 2020の13日目の記事です。

 

2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。

 

 

[assertFailsWithで例外をテスト]

HTTPメソッドはGET、POST、DELETEなど8つと決まっています。Getのように小文字が含まれている場合も無効なHTTPメソッドとなります。無効なHTTPメソッド文字列が送られてきた場合には、400 Bad RequestのHTTPレスポンスを返さなくてはなりません。

RFC 7230 - 3.1.1. Request Line

 

これは、HTTPサーバーがHTTPレスポンスを受け取ってstart-linerequest-lineの代わりにstatus-lineであった場合も同様です。

RFC 7230 - 3.1. Start Line

 

ReequestParser.ktに、この機能を追加します。

まず、ReequestParser.ktファイルに、以下のようにBadRequestExceptionを定義します。

abstract class HttpException(val status: Int, val reason: String): RuntimeException()

class BadRequestException(): HttpException(400, "Bad Request")

 

つぎに、テストケースを追加します。

 

JavaJUnitを利用している場合は以下のように、@Testアノテーションに、expectedフィールドを使って発生させたい例外クラスを指定することで、例外をテストすることができました。

@Test(expected = IllegalArgumentException.class)
public void testFailsWithIllegalArgumentException() {
    // do some test
}

 

Kotlinでも同様に実装できるかと思い試してみましたが、コンパイルエラーになってしまいました。

f:id:kidani_a:20201213163025p:plain


Googleで「kotlin test exception expect」と検索してみると、公式サイトが見つかりました。assertFailsWithという関数を使えば例外のテストを実装できそうです。

assertFailsWith - Kotlin Programming Language

 

ただ、公式サイトをみても実装方法が理解できなかったので、以下のブログを参考にしました。

su-kun1899.hatenablog.com

assertFailsWith<IllegalArgumentException> { someMethod(invalidArg) }

 

これを参考に、BadRequestExceptionが投げられるかどうか確認するテストを以下のように実装します。

@Test
fun shouldNotParseHttpResponse() {
    val resByteArray = ("HTTP/1.1 200 OK" + crlf
            + crlf).encodeToByteArray()
    val actual = assertFailsWith<BadRequestException> { parser.parse(resByteArray) }
    assertEquals(400, actual.status)
    assertEquals("Bad Request", actual.reason)
}

 

テストを実行すると、BadRequestExceptionを投げる部分は実装されていないので、以下のようにテストが失敗します。

kotlin.AssertionError: Expected an exception of com.kdnakt.kttpd.BadRequestException to be thrown, but was kotlin.Exception: Invalid enum value name: HTTP/1.1

 

enumとして定義したHttpMethodnameに合致しない文字列が引数として渡されたため、HttpMethod.valueOf()の部分がIllegalArgumentExceptionを投げています。

これは、単純に以下のように修正することもできます。

try {
    return RequestContext(
            HttpMethod.valueOf(startLineElements[0]),
            startLineElements[1],
            httpMethod
    )
} catch (e: IllegalArgumentException) {
    e.printStackTrace()
    throw BadRequestException()
}

 

しかし、try-catchブロックで囲むコード量が多く、他の例外もキャッチしてしまいそうです。文字列からenumを生成するのに良い方法はないか探したところ、以下のStack OverflowでfirstOrNullを利用したコードを見つけました。

 

stackoverflow.com

 

これを参考に、以下のように実装を修正します。nullだった場合には、エルビス演算子?:を利用してBadRequestExceptionを投げます。

val httpMethod = HttpMethod.values()
    .firstOrNull { it.name == startLineElements[0] }
    ?: throw BadRequestException()

 

これで無事テストが成功しました。

f:id:kidani_a:20201213170327p:plain

 

[まとめ]

  • KotlinではJavaJUnitと異なり、assertFailsWithで例外をテストする
  • assertFailsWith<IllegalArgumentException> { someMethod(invalidArg) }のように実装する
  • try-catchの代わりにエルビス演算子?:を利用して例外処理を実装した
  • 実装中のコードは以下のリポジトリにまとめてある

github.com