KotlinTestをAndroidで使ってみる

これは [Kotlin Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/kotlin) の22日目の記事です。

## はじめに
[Android Test Night #2](https://testnight.connpass.com/event/69397/) で発表した [Try KotlinTest](https://speakerdeck.com/rkowase/try-kotlintest) とほぼ同じ内容です。

## KotlinTestとは
![](https://qiita-image-store.s3.amazonaws.com/0/123122/9cf29902-63ed-ef4d-177e-294d51da3379.png)

KotlinTest is a flexible and comprehensive testing tool for Kotlin.

リポジトリ: https://github.com/kotlintest/kotlintest

## Kotlin用アサーションライブラリ（テストフレームワーク）一覧

|名前|GitHub Star|簡単な説明|
|---|---|---|
|[Spek](https://github.com/spekframework/spek)|970|Spec風、Java 8 Only (参考: [Spek User Guide](http://spekframework.org/docs/latest/#_android))|
|[KotlinTest](https://github.com/kotlintest/kotlintest)|593|Spec風|
|[expekt](https://github.com/winterbe/expekt)|96|メソッドチェーン記法、更新頻度低|
|[AssertK](https://github.com/willowtreeapps/assertk)|78|AssertJ風|
|[knit](https://github.com/ntaro/knit)|57|DroidKaigi 2017アプリで利用、更新頻度低|

（2017/12/15時点のStar数です）

## 導入
```
dependencies {
    testImplementation 'io.kotlintest:kotlintest:2.0.7'
}
```

## 基本的な使い方
任意のSpec（下記の例だと`StringSpec`）を継承して、initブロックにテストケースを書いていきます。

```kotlin
// test cases in init block
class MyTests : StringSpec() {
  init {
    // tests here
  }
}
```

後述するInterceptingなどを使わない場合（initブロックのみの場合）、下記のようにラムダ記法でも書けます。

```kotlin
// test cases in lambda expression
class MyTests : StringSpec({
  // tests here
})
```

## 色々なSpec

Spec|説明
---|---
String Spec|StringSpec reduces the syntax to the absolute minimum.
Fun Spec|FunSpec allows you to create tests similar to the junit style. 
Should Spec|ShouldSpec is similar to fun spec, but uses the keyword should instead of test.
Word Spec|WordSpec uses the keyword should and uses that to nest test blocks after a context string.
Feature Spec|FeatureSpec allows you to use feature and scenario.
Behavior Spec|BehaviorSpec allows you to use given, when, then.
Free Spec|FreeSpec allows you to nest arbitary levels of depth using the keyword - (minus).

### String Spec
一番シンプルなSpecです。

```kotlin
class MyTests : StringSpec() {
  init {
    "strings.length should return size of string" {
      "hello".length shouldBe 5
    }
  }
}
```

### Fun Spec
JUnit風に書けるSpecです。

```kotlin
class MyTests : FunSpec() {
  init {
    test("String.length should return the length of the string") {
      "sammy".length shouldBe 5
      "".length shouldBe 0
    }
  }
}
```

### Should Spec
前述のFun Specの`test`の代わりに`should`を使ったSpecです。

```kotlin
class MyTests : ShouldSpec() {
  init {
    should("return the length of the string") {
      "sammy".length shouldBe 5
      "".length shouldBe 0
    }
  }
}
```

### Word Spec
コンテキスト毎に`should`キーワードでネストできるSpecです。

```kotlin
class MyTests : WordSpec() {
  init {
    "String.length" should {
      "return the length of the string" {
        "sammy".length shouldBe 5
        "".length shouldBe 0
      }
    }
  }
}
```

### Feature Spec
`feature`と`scenario`キーワードを使ったSpecです。

```kotlin
class MyTests : FeatureSpec() {
  init {
    feature("the thingy bob") {
      scenario("should explode when I touch it") {
        // test here
      }
      scenario("and should do this when I wibble it") {
        // test heree
      }
    }
  }
}
```

### Behavior Spec
`given`, `when`, `then`を使ったSpecです。
Spekのdescribe-on-it記法のような書き方ができます。

```kotlin
class MyTests : BehaviorSpec() {
  init {
    given("a broomstick") {
      `when`("I sit on it") {
        then("I should be able to fly") {
          // test code
        }
      }
      `when`("I throw it away") {
        then("it should come back") {
          // test code
        }
      }
    }
  }
}
```

### Free Spec
`-`（ハイフン）を使ったSpecです。

```kotlin
class MyTests : FreeSpec() {
  init {
    "String.length" - {
      "should return the length of the string" {
        "sammy".length shouldBe 5
        "".length shouldBe 0
      }
    }
  }
}
```

## Intercepting a Test Case
`interceptTestCase()`をoverrideすることで、JUnitでいう`setUp()`と`tearDown()`をすることができます。
コメントの`before`のところに前処理、`after`のところに後処理を書きます。

```kotlin
class MyTests : StringSpec() {
  override fun interceptTestCase(context: TestCaseContext, test: () -> Unit) {
    // before
  
    test() // don't forget to call test()!
  
    // after
  } 
  init {
    "strings.length should return size of string" {
      "hello".length shouldBe 5
    }
  }
}
```

### Presenterのテスト
とりあえず一番シンプルなStringSpecで使ってみました。
[既存コードをAndroid非依存なクラスに抽象化してユニットテストするための第一歩](https://qiita.com/rkowase/items/54dc44537e9af519023e)のテストコードを元にしてます。


```kotlin
class StringSpecTest : StringSpec() {
   @Mock lateinit var mRepository: GitHubRepository
   @Mock lateinit var mView: GitHubContract.View

   lateinit var mPresenter: GitHubPresenter
   lateinit var mSchedulerProvider: BaseSchedulerProvider

   override fun interceptTestCase(context: TestCaseContext, test: () -> Unit) {
       MockitoAnnotations.initMocks(this)
       mSchedulerProvider = ImmediateSchedulerProvider()
       mPresenter = GitHubPresenter(mRepository, mView, mSchedulerProvider)

       test() // don't forget to call test()!
   }

  init {
     "Should init service when presenter started"{
         mPresenter.start()
         verify(mRepository).initService()
     }
     "Should show error when request failed"{
        Mockito.`when`(mRepository.request("")).thenRetrn(Observable.error(Exception())
        mPresenter.request(GitHubPresenterTest.USER)
        verify(mView).showError()
     }
  }
}
```

## 気になるところ
- StringSpec以外だとlateinitな変数がInterceptingできない（？）
  - https://github.com/kotlintest/kotlintest/issues/174 
- BehaviorSpecがgiven-when-thenなので、予約語のwhenをエスケープ（`when`）しないといけない
  - Spekはdescribe-on-it形式なのでエスケープ不要

## 参考
Try KotlinTest // Speaker Deck
https://speakerdeck.com/rkowase/try-kotlintest

kotlintest/reference.md at master · kotlintest/kotlintest
https://github.com/kotlintest/kotlintest/blob/master/doc/reference.md

Spek User Guide
http://spekframework.org/docs/latest/