はじめに
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割る2は5() {
assert(calc.divide(10, 2) == 5)
}
@Test
fun テスト_10を0で割ると例外() {
assertThrows<ArithmeticException> { calc.divide(10, 0) }
}
}
JUnitはメソッド名を数字始まりにできないんだった。
階層化してみる。
JUnit5で簡単になったとは言え、かなり冗長な感じがする。
class CalcTest_JUnit5 {
private val calc = Calc()
@Nested
inner class Calcのテスト {
@Nested
inner class 正常系{
@Test
fun テスト_10割る2は5() {
assert(calc.divide(10, 2) == 5)
}
}
@Nested
inner class 異常系{
@Test
fun テスト_10を0で割ると例外() {
assertThrows<ArithmeticException> { calc.divide(10, 0) }
}
}
}
}
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) }
}
}
}
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) }
}
}
}
}
}
以後は階層化したものだけ記載する。
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) }
}
}
} }
}
公式では
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) }
}
}
}
}
}
以前は階層化する際に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
describe
、context
、it
というキーワードでテストを定義する。
キーワードはそれぞれ複数回使うこともできる(逆に使わなくても良い)。
大昔に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) }
}
}
}
}
}
Behavior Spec
given
、when
、then
というキーワードでテストを定義する。
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) }
}
}
}
}
}
given
、when
はそれぞれ複数回使ったり、更に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) }
}
}
}
}
}
Feature Spec
feature
、scenario
というキーワードでテストを定義する。
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) }
}
}
}
}
}
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) }
}
}
}
}
}
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割る2は5() {
assert(calc.divide(10, 2) == 5)
}
}
@Nested
inner class 異常系{
@org.junit.jupiter.api.Test
fun テスト_10を0で割ると例外() {
assertThrows<ArithmeticException> { calc.divide(10, 0) }
}
}
}
}
しかしIntelli Jで実行するとなぜか一番外側のクラス単位にまとめられてしまった。
原因は不明。
最後に
他のテストフレームワーク(といってもGoogletestとJUnitを少しかじっただけど)と比べて、機能的に絶対的なメリット/デメリットはなくても、テストを簡潔に書けることや可読性にフォーカスしている感じがイマドキっぽく感じた。
Javaと混在のプロジェクトでは敢えてJUnitから乗り換えるほどのメリットはないと思うけど、ピュアなKotlinプロジェクトならこっちに寄せるのも面白そう。
ただし微妙に枯れていない感じもあって、公式に載っていてもちゃんと動かない機能があったり、ググっても情報が少なかったりするのでハマる危険は相対的に大きいと思う。
(まさにこれをやってExposedで絶賛ハマリ中。。そういえばJavalinでもハマった。)
Kotestの機能的な話については、もう少し使い込んで情報がまとまったら別の記事にする。かも。