21
11

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.

Kotestの各種Specを比べてみた

Last updated at Posted at 2020-06-08

はじめに

KotlinのプロジェクトでKotestを使ってテストを書いてみた時に
Specって何?とか、結局何を使えばいいの? で迷ったので調べたことをメモしておく。

Kotestとは

Kotlinで使えるテストフレームワーク。
Javaに対するJUnitのような存在。
以前はKotlintestという名前だったがRelease 4.0からKotestに改名された。

後発だけあって、他の色々なテストフレームワークから良い所を取り込んでいたり
テストを色々な書き方(これがSpec)ができるのが特徴。

サンプルテストコード(公式より)
class MyTests : StringSpec({
  "length should return size of string" {
    "hello".length shouldBe 5
  }
  "startsWith should test for a prefix" {
    "world" should startWith("wor")
  }
})

長くなったので先に結論

  • Specとは『テストを記述するスタイル』のこと
    基本的にはどのSpecを使っても機能的な大きく違いはない(ちょっとはある)
    Specが複数ある理由は他のフレームワークで慣れた方法で書けるため
  • ぶっちゃけどれでも好きなのを使えば良い
  • それでも迷った場合には
    • 慣れた書き方があるならそれで書く
    • ネストが不要なら『String Spec』でOK
    • ネストが必要なら個人的には『Free Spec』がおススメ

環境

  • Kotlin:1.3.61
  • Kotest:4.0.6
  • JUnit5:5.6.2
  • Intelli J:2019.3(Community Edition)

Specの一覧

Spec 特徴
String Spec 一番基本的なSpec
Fun Spec ScalaTestと同じ書き方
Expect SPec Fun Specに似ている
Should Spec Fun Specに似ている
Describe Spec RSpecと同じ書き方
Behavior Spec BDD frameworksと同じ書き方
Word Spec ScalaTestと同じ書き方
Feature Spec Cucumberと同じ書き方
Free Spec ScalaTestと同じ書き方
Annotation Spec JUnitと同じ書き方

サンプル

テスト対象クラス

割り算するだけのクラス。

class Calc {
    fun divide(a: Int, b:Int): Int {
        return a / b
    }
}

JUnit5で書くとこんな感じ

class CalcTest_JUnit5 {
    private val calc = Calc()

    @Test
    fun テスト_10割る25() {
        assert(calc.divide(10, 2) == 5)
    }

    @Test
    fun テスト_100で割ると例外() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
}

JUnitはメソッド名を数字始まりにできないんだった。

実行結果
image.png

階層化してみる。
JUnit5で簡単になったとは言え、かなり冗長な感じがする。

class CalcTest_JUnit5 {
    private val calc = Calc()

    @Nested
    inner class Calcのテスト {
        
        @Nested 
        inner class 正常系{
            @Test
            fun テスト_10割る25() {
                assert(calc.divide(10, 2) == 5)
            }
        }

        @Nested
        inner class 異常系{
            @Test
            fun テスト_100で割ると例外() {
                assertThrows<ArithmeticException> { calc.divide(10, 0) }
            }
        }
    }
}

実行結果
image.png

String Spec

公式でもおススメっぽく全ての機能が動く&ネットでの情報も豊富。
(他のSpecは未サポート機能が割とある)

が、非常に残念ながら階層化できない。
人によっては割と致命的かもしれない?

class StringSpecのテスト : StringSpec() {
    private val calc = Calc()

    init {
        "10 割る 5 は 2 になる" {
            calc.divide(10, 5) shouldBe 2
        }

        "10 割る 0 は例外(ArithmeticException)が起きる" {
            shouldThrow<ArithmeticException> { calc.divide(10, 0) }
        }
    }
}

実行結果
image.png

Fun Spec

test というキーワードでテストを定義する。

class FunSpecのテスト : FunSpec() {
    private val calc = Calc()

    init {
        test("10 割る 5 は 2 になる") {
            calc.divide(10, 5) shouldBe 2
        }
        
        test("10 割る 0 は例外(ArithmeticException)が起きる") {
            shouldThrow<ArithmeticException> { calc.divide(10, 0) }
        }
    }
}

context を使って階層化することもできる。

class FunSpecのテストをネストする : FunSpec() {
    private val calc = Calc()

    init {
        context("Calcのテスト") {
            context("正常系") {
                test("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            context("異常系") {
                test("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

以後は階層化したものだけ記載する。

Expect Spec

Fun Specと似ている。

testの代わりにexpectというキーワードでテストを定義する。
context を使って階層化することもできる。

class ExpectSpecのテスト : ExpectSpec() {
    private val calc = Calc()
    
    init {
        context("Calcのテスト") {
            context("正常系") {
                expect("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            context("異常系") {
                expect("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }    }
}

実行結果
image.png

公式では

Tests can be disabled using the xcontext and xexpect variants

と、X-Methodsを使って無効化できると書いてあるが実際には動作しなかった。
公式のサンプルも間違っているので、このExpectSpecではX-Methodsは使えないっぽい。

class MyTests : DescribeSpec({  // ★ 例がExpectSpecじゃなくてDescribeSpecになっている
    context("this outer block is enabled") {
        xexpect("this test is disabled") {
            // test here
        }
    }

Should Spec

Fun Specと似ている。

testの代わりにshouldというキーワードでテストを定義する。
context を使って階層化することもできる。

class ShouldSpecのテスト : ShouldSpec() {
    private val calc = Calc()

    init {
        context("Calcのテスト") {
            context("正常系") {
                should("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }

            }
            context("異常系") {
                should("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

以前は階層化する際にcontextを使わず直接文字列で階層を定義できたが
非推奨になったっぽい。(一応動くけど、IDEAに怒られる)

class ShouldSpecのテスト_非推奨な書き方 : ShouldSpec() {
    private val calc = Calc()

    init {
        "Calcのテスト" {
            "正常系" {
                should("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }

            }
            "異常系" {
                should("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

Describe Spec

describecontextit というキーワードでテストを定義する。
キーワードはそれぞれ複数回使うこともできる(逆に使わなくても良い)。

大昔にC++のプロジェクトでiglooというフレームワークを使っていたのを思い出した。

class Describeのテスト() : DescribeSpec() {
    private val calc = Calc()
    
    init {
        describe("Calcのテスト") {
            context("正常系") {
                it("10 割る 5 は 2 になる") {
                    calc.divide(10, 5).shouldBe(2)
                }
            }
            context("異常系") {
                it("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

Behavior Spec

givenwhenthen というキーワードでテストを定義する。
BDDスタイルの書き方。

Kotlinではwhenが予約語になっているため`when`みたいにバッククォートで括るか
もしくは先頭を大文字にしてWhenと書く。
(他の予約語も先頭を大文字にできる)

class BehaviorSpecのテスト : BehaviorSpec() {
    private val calc = Calc()
    
    init {
        Given("Calcのテスト") {
            When("正常系") {
                Then("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            When("異常系") {
                Then("10 割る 0 は例外(ArithmeticException)が起きる") {
                    shouldThrow<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

givenwhenはそれぞれ複数回使ったり、更にandというキーワードもつかって階層化できるが
Intelli Jで実行するとテスト結果自体は階層化されていないため、こちらは可読性のためだけに使えるっぽい。

Word Spec

任意の文字列とshould というキーワードでテストを定義する。
Word Specなのにshouldを使うのが紛らわしい。。。

shouldの外側に1階層だけwhenを使って階層化できる。
またshouldの内側にも1階層だけ任意の文字列で階層化できる。

class WordSpecのテスト : WordSpec() {
    private val calc = Calc()
    
    init {
        "Calcのテスト" When {
            "正常系" should  {
                "10 割る 5 は 2 になる"  {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            "異常系" should {
                "10 割る 0 は例外(ArithmeticException)が起きる"  {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

Feature Spec

featurescenario というキーワードでテストを定義する。
cucumberスタイルの書き方。

featureはネストすることができる。

ExpectSPecと同様にX-Methodsによる無効化は動作しない。

class FeatureSpecのテスト : FeatureSpec() {
    private val calc = Calc()

    init {
        feature("Calcのテスト") {
            feature("正常系") {
                scenario("10 割る 5 は 2 になる") {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            feature("異常系") {
                feature("10 割る 0 は例外(ArithmeticException)が起きる") {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }                    
                }
            }
        }
    }
}

実行結果
image.png

Free Spec

任意の文字列と- というキーワードでテストを定義する。
文字列は任意の数だけ階層化できる。

『階層化できるString Spec』に近いのか?

キーワードを強制されず好きなだけ階層化できるのでシンプルに書けて自由度は高いが
一方で無法地帯になって可読性が落ちる危険もある。

class FeeeSpecのテスト : FreeSpec() {
    private val calc = Calc()

    init {
        "Calcのテスト" - {
            "正常系" - {
                "10 割る 5 は 2 になる" - {
                    calc.divide(10, 5) shouldBe 2
                }
            }
            "異常系" - {
                "10 割る 0 は例外(ArithmeticException)が起きる" - {
                    assertThrows<ArithmeticException> { calc.divide(10, 0) }
                }
            }
        }
    }
}

実行結果
image.png

Annotation Spec

JUnitそのままの書き方。

公式にすら

特にアドバンテージはない

と書かれている通り、JUnitの書き方に慣れた人がKotestで書きたい場合に使うことを意図しているらしいが
じゃそもそもなんでKotestを使うの? という気が。。

@Nasted アノテーションによる階層化も普通にできた。
テストメソッドをinit{}で括らなくて良いのが他のSpecと少しだけ違う。

class AnnotationSpecのテスト : AnnotationSpec() {
    private val calc = Calc()

    @Nested
    inner class Calcのテスト {

        @Nested
        inner class 正常系{
            @org.junit.jupiter.api.Test
            fun テスト_10割る25() {
                assert(calc.divide(10, 2) == 5)
            }
        }

        @Nested
        inner class 異常系{
            @org.junit.jupiter.api.Test
            fun テスト_100で割ると例外() {
                assertThrows<ArithmeticException> { calc.divide(10, 0) }
            }
        }
    }
}

しかしIntelli Jで実行するとなぜか一番外側のクラス単位にまとめられてしまった。
原因は不明。

実行結果
image.png

最後に

他のテストフレームワーク(といってもGoogletestとJUnitを少しかじっただけど)と比べて、機能的に絶対的なメリット/デメリットはなくても、テストを簡潔に書けることや可読性にフォーカスしている感じがイマドキっぽく感じた。
Javaと混在のプロジェクトでは敢えてJUnitから乗り換えるほどのメリットはないと思うけど、ピュアなKotlinプロジェクトならこっちに寄せるのも面白そう。

ただし微妙に枯れていない感じもあって、公式に載っていてもちゃんと動かない機能があったり、ググっても情報が少なかったりするのでハマる危険は相対的に大きいと思う。
(まさにこれをやってExposedで絶賛ハマリ中。。そういえばJavalinでもハマった。)

Kotestの機能的な話については、もう少し使い込んで情報がまとまったら別の記事にする。かも。

参考情報

21
11
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
21
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?