Kotlin+Junitでユニットテストする方法

ソフトウェアテストとは何か?

ユニットテスト

メソッド単位のテスト

結合テスト

メソッドの組み合わせのテスト

UIテスト

画面を動かしながら不具合の検知を行う。

クラスやメソッドを対象とした最も小さいテスト:ユニットテスト

開発に不安が残らないようになる。

ユニットテストは実行できるプログラムであり、そのフレームワークがある。

今回はJUnitというフレームワークを使う(Javaユニットテスト出もある)

ユニットテストとは?

ソフトウェアテストの一つ

クラスやメソッドを対象としたテスト

ソフトウェアのふるまいを、条件を固定して検証する

開発者が意図したとおりにプログラムが動くのかを確認し、期待通りに動かない箇所を検知する。

品質の保守が目的

ソフトウェアの納品・リリース後に重大なバグが発生すると才アックのケースで損害賠償が発生する。

テストケースが多いほど信頼性が高くなり、バグの発見する確率が高くなる。

このことを、網羅性が高くなると言われる。

引数の足し算を行うメソッドを検証する。

  • 1+2だけだと不安

  • 1.0~9.0の9*9の81通りのテストを行う方がいい

しかし、全ての組み合わせを検証するのは現実的ではない。

「完璧なテストはできない」という前提を持ち、少しでも多くのバグを見つける(best effort)という気持ちで行う

tutorial.ktファイルが作成された後

package com.example.tutorial

class Calculator {
    fun multiply(x: Int, y:Int): Int {
        return x*y
    }
    fun divide(x: Int, y:Int): Int {
        return x/y
    }
}

これらのクラスをテストしていく

テストクラスとテストメソッド

テスト対象のクラス

テストの作り方

テストしたいクラスのクラス名にカーソルを合わせたうえで、「Ctrl+Shift+T」を押す

すると「Create New Test...」と出現する

クリックすると

Testling Library:JUnit

Class name Caluclatortest

...のプロパティが出現する

「メソッドに付随するチェックボックスは全てチェックする」

次にようなコードができれば完成

class CalculatorTest {

    @Test
    fun multiply() {

    }

    @Test
    fun divide() {

    }
}

コードの説明

@Testと書かれているのは「アノテーション」と呼ばれる者である。

このアノテーションが書かれているものが「テストメソッド」と認識される。

ソフトウェアテストでは期待通りの振る舞いをするかどうかを検証

期待値と実測値を比較するのが良い。

このような値の比較検証をアサーションという

JUnitアサーションを行うにはアサーションライブラリを用いるのが便利

今回は直感的で自然言語に近い記述ができる「AssertJ」を使う

  1. Gradle Scriptsの「build.gradle(app)」を開き、以下のように追記する
dependencies {
    ...
    testImplementation 'junit:junit:4.12'
    testImplementation 'org.assetj:assertj-ccore:3.10.0' (追加部分)
    ...
}
  1. import部分に次の記述を追加
...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiply() {

    }

    @Test
    fun divide() {

    }
}
  1. import部分のassertJが赤いので、上のタブの方に注意書きが出るので「Sync Now」をクリック

  2. assertJが青くなれば完了

テストコードの注意点

主な手順は以下の二つ

  1. 実測値と期待値を用意しておく

  2. それらを比較する

これらを用意していきたいが、JUnit4はいくつかルールがある。

  1. テストクラスはpublicクラス

  2. テストメソッドはorgjunit.Testアノテーションを付与したpublicメソッド

  3. 戻り値を持たないこと

以下はダメな例

...
import org.assertj.core.api.Assertions.*
...

private class CalculatorTest { //privateはアウト

    @Test
    private fun multiply() { //privateはアウト

    }

    @Test
    fun divide(set) { //引数はだめ

        return "true" //戻り値もダメ
    }
}

テストメソッドを書いていく

  1. メソッド名は日本語でも一応問題はない
...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {

    }

    @Test
    fun divide() {

    }
}
  1. 具体的な処理はassertionで比較

実測値と予測値が等しいという自然言語に近い記述

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        // sutはテスト対象オブジェクトという意味
        // よく使われる変数名なので覚えておいて損はない
        val sut = Calculator()
        val actual = sut.multiply(x:2, y:3)
        val expected = 6
        // 実測値と予測値が等しいという自然言語に近い記述
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun divide() {

    }
}

ポイント

  • sutはテスト対象オブジェクトという意味

よく使われる変数名なので覚えておいて損はない

  • 実測値と予測値が等しいという自然言語に近い記述

actualとexpectedも良く使われるので覚えておく

実行

SHIFT+F10でテストケースの実行

実行後、新たにウィンドウが出現し実行結果が表示される。

色は緑でありこれを「グリーンバー」と呼ばれる

テストが間違えたとき

expectedを3に変更して見る

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        // sutはテスト対象オブジェクトという意味
        // よく使われる変数名なので覚えておいて損はない
        val sut = Calculator()
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        // 実測値と予測値が等しいという自然言語に近い記述
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun divide() {

    }
}

このように実行すると「expectedは3だが、actualは5である」と表記される

色は赤になり「レッドバー」と呼ばれる

divideメソッドについてもやってみる

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        // sutはテスト対象オブジェクトという意味
        // よく使われる変数名なので覚えておいて損はない
        val sut = Calculator()
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        // 実測値と予測値が等しいという自然言語に近い記述
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun divide() {
        val sut = Calculator()
        val actual = sut.divide(6,2)
        val expected - 3
        assetThat(actual).isEqualTo(expected)
    }
}

テストケースで想定される失敗

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        // sutはテスト対象オブジェクトという意味
        // よく使われる変数名なので覚えておいて損はない
        val sut = Calculator()
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        // 実測値と予測値が等しいという自然言語に近い記述
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun dividewoで3を2で割った結果を取得できる() {
        val sut = Calculator()
        val actual = sut.divide(6,2)
        val expected - 3
        assetThat(actual).isEqualTo(expected)
    }

    @Test
    fun divide() {
        val sut = Calculator()
        val actual = sut.divide(3,2)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

これは失敗する

「期待値は1.5になっているが、1になっているのがポイント」

普段からプログラミングをやっていれば「Intで型定義されている」と判断できる。

Kotlinでは整数/整数は整数となっているため、どちらかを少数型(Double型)に替えなければならない。

テスト中に例外が発生した場合

コードを以下のように変換する

class Calculator {
    fun multiply(x: Int, y:Int): Int {
        return x*y
    }
    fun divide(x: Int, y:Int): Int {
        if ( y == 0) throw IllegalArgmentException("divide by zero.")
        return x/y
    }
}

このようにすることで、yに0が入ったときでも意図的にエラーを発生させることができる。

以下のテストは「3を0で割っているため」ZeroDivizionErroが発生する

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun divideで3を0で割った場合() {
        val sut = Calculator()
        val actual = sut.divide(3,0)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

テスト側も、実測値と期待値の比較では検証することができない。 (そもそもassetThatの行までたどり着かない。)

このテストを次のように変換する

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test(expected = IllegalArgumentException: class)
    fun divideで3を0で割った場合() {
        val sut = Calculator()
        val actual = sut.divide(3,0)
    }
}

Testアノテーションのexpectedはエラーが発生する例外クラスを入れて検知できる。

テストコードがテスト設計に及ぼすメリット

ここまでの対応で

テスト設計の品質が上がったことをが大事。

プログラムレベルでテストコードを記述することで「割り算ができる」という抽象的な仕様ではなく

「3を2で割ったら1.5を返す」 「0っで割られたらIllegalargumentExceptionを送る」

という曖昧さのない具体的な仕様を定めることができる。

ユニットテストJUnitの基本

ユニットテストはなぜ行う必要があるのか?

  1. 曖昧さのない正確な仕様書として機能するため

具体的なテストケースを通すプロダクトコードを書く必要があるので、 ユニットテストのテストケース自体が曖昧さのない仕様書として機能する

  1. 進捗のフィードバックとして機能する

テストを実行しながら並行で開発をするめることで、開発の不安が少なくなる。


結果としてソフトウェアの品質が向上する

備考

title:KotlinとJUnitでテストコードを書いてみる

description:開発に不安が残らないようになる。ユニットテストは実行できるプログラムであり、そのフレームワークがある。今回はJUnitというフレームワークを使う(Javaユニットテスト出もある)

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

ユニットテスト

ユニットテストはなぜ行う必要があるのか?

  1. 曖昧さのない正確な仕様書として機能するため

具体的なテストケースを通すプロダクトコードを書く必要があるので、 ユニットテストのテストケース自体が曖昧さのない仕様書として機能する

  1. 進捗のフィードバックとして機能する

テストを実行しながら並行で開発をするめることで、開発の不安が少なくなる。


結果としてソフトウェアの品質が向上する

良いユニットテストの条件5つ

  1. 仕様書として読めるように分かりやすく書かれている

  2. 可読性を維持するためにテストコードが適切に整理されている

  3. 問題を特定できるレベル、粒度で書く

  4. テストケース同士で依存が発生していない

  5. 成功するか失敗するか分からない不安定なテストが放置されていない

仕様書として読めるように分かりやすく書かれている

テストメソッド名を具体的に表現したり、メソッドの中野記述もなるべくテスト意図が伝わるように意識して記述する

可読性を維持するためにテストコードが適切に整理されている

JUnit等のテスティングフレームワークを使った気受した、 自動化されたテストコードはプロダクトコード同様、きちんと管理する必要がある。

問題を特定できるレベル、粒度で書く

テストメソッドが大きすぎると、テストが失敗した原因が分かりずらい。

一つのテストに対しては、一つの問題を扱う様にしよう

テストケース同士で依存が発生していない

テストの結果が他のテストやオブジェクトに依存していては、テスト対象以外位の要素で結果が変わってしまう。 テストAの成功結果がメソッドBの結果で左右されてしまう等。

成功するか失敗するか分からない不安定なテストが放置されていない

不安定なテストは「レッドバー」を無視するような文化が発生してしまう。 開発の進捗状況は「グリーンバー」の数でフィードバックしよう。

ソフトウェアテストの4フェーズ

  1. 事前準備

  2. 実行

  3. 検証

  4. 後処理

この4フェーズにテストユニットは分かれる。

事前準備

テスト対象オブジェクトの(SUT)の初期化、入力や期待値の用意

実行

SUTのテストの操作を行う

検証

テストの結果として得られた実測値が期待する結果と一致するか比較検証

後処理

次のテストの実行に影響が出ないようにする

@Test
fun 要素が二つの時にremoveAtで先頭要素を除くとsizeが1になる(){
    // 事前準備
    val sut = mutableListOf<string>()
    sut.add("Hello")
    sut.add("World")
    // 2実行
    sut.removeAt(1)
    // 比較
    assertThat(sut).hasSize(1)
    assertThat(sui[0]).isEqualTo("World")
}

(備考)二種類のテスト

ソフトウェアテストはテストケースの作り方によって大きく二種類に分かれる

内部のロジックや仕様を考慮してテストケースを作成する ロジックなどを読み取れる必要があるので、ある程度プログラミングの知識が必要となる。

内部のロジックが正しく動いているのかまで把握する。

デメリット)テストコードが内部構造に強く依存してしまうと、テストコードがプロダクトコードの変更に影響が与えられる。

ソフトウェアの内部のロジックや仕様を考慮せず、外部から見たときの仕様のみからテストケースを作成する

業務の知識が必要。

デメリット)入力が簡単だが、複雑なテストをできない

ユニットテストのパターン

ユニットテストはいくつかのパターンがある

  1. 標準的な振る舞いを検証するテスト

初期化・実行・検証・後処理の4つのフェーズで構成される最も一般的なテスト

  1. 例外を検証するテスト(異常系)

メソッドの実行時に例外が想定通りに投げられる事を検証するテスト

  1. コンストラクタを検証するテスト

テスト対象クラスのインスタンスを生成してコンストラクタを検証するテスト

@Test
fun コンストラクタのテスト() {
    val taro = Person("Taro, 30)
    assertThat(taro.name).isEqualTo("Taro")
    assertThat(taro.age).isEqualTo(30)
}

備考

title:KotlinとJUnitの良いテストを書くコツ

description:1. 曖昧さのない正確な仕様書として機能するため。具体的なテストケースを通すプロダクトコードを書く必要があるので、ユニットテストのテストケース自体が曖昧さのない仕様書として機能する。2. 進捗のフィードバックとして機能する。テストを実行しながら並行で開発をするめることで、開発の不安が少なくなる。

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

JUnitアノテーションの種類

JUnitoには@Testアノテーションを付与するが、そのほかのアノテーションも存在する

@Ignoreアノテーション

あるテストを実行対象外としたいときのアノテーション

Ignoreアノテーションが付いたテストメソッド/クラスは実行されなくなる

以下のように、Testアノテーションの上にかぶせる形で付与する

@Ignore
@Test
fun 一時的に無視するテスト

@Beforeアノテーション

共通の初期化処理は、Beforeアノテーションを付与したメソッドにまとめることができる。 戻り値と引数を持たないpublicメソッドが必要であり、setUpというメソッド名がよく使われる。

例えば、以下のテストでは、Calculatorクラスをインスタンス化する処理が全てのテストに入っている

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        // sutはテスト対象オブジェクトという意味
        // よく使われる変数名なので覚えておいて損はない
        val sut = Calculator()
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        // 実測値と予測値が等しいという自然言語に近い記述
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun dividewoで3を2で割った結果を取得できる() {
        val sut = Calculator()
        val actual = sut.divide(6,2)
        val expected - 3
        assetThat(actual).isEqualTo(expected)
    }

    @Test
    fun divide() {
        val sut = Calculator()
        val actual = sut.divide(3,2)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

これをBeforeアノテーションを使って初期化しようとすれば以下のようになる。 setUpの中でインスタンス化したので、他のメソッドでインスタンス化する必要はなくなる。

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {
    lateinit var sui: Calculator

    @Before
    fun setUp () {
        sut = Calculator()
    }

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun dividewoで3を2で割った結果を取得できる() {
        val actual = sut.divide(6,2)
        val expected = 3
        assetThat(actual).isEqualTo(expected)
    }

    @Test
    fun divide() {
        val actual = sut.divide(3,2)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

@Afterアノテーション

共通の後処理があるなら、Afterアノテーションにまとめることができる。 Beforeアノテーションの時と同様、引数を持たないpublicメソッドであり、tearDownが使われる。

@After
tearDown (){}

備考

title:JUnitでよく使うアノテーションの種類

description:@Beforeアノテーションは、共通の初期化処理は、Beforeアノテーションを付与したメソッドにまとめることができる。戻り値と引数を持たないpublicメソッドが必要であり、setUpというメソッド名がよく使われる。

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

アサーション

実測値と期待値を比較検証するプロセスのこと

ユニットテスト、ひいてはソフトウェアテストにおいて最も重要な概念。

JUnitではAssertJのようなアサーションライブラリを用いることで、 自然言語に近い直感的な記述が可能になる

ここではAssertJの基本的な使い方について扱う

文字列のアサーション

"KOTLIN"という文字列を様々な条件で比較検証してみる

assertThat("KOTLIN")
    .`as`("KOTLINの文字列チェック")
    .isEqualTo("KOTLIN")
    .isEqualToIgnoreingCase("kotlin")
    .isNotEqualTo("KOLTIN")
    .startsWith("KO")
    .endsWith("IN")
    .contains("TL")
    .isInstanceOf(String:class.java)

アサーションにラベルを付ける.as(...)

assertThat("KOTLIN")
    .`as`("KOTLINの文字列チェック")

アサーションにラベルを付けると、テストが失敗した時にここで定義したラベル名が表示されるので、 どのアサーションが失敗したのか分かりやすくなる。

AssertJの「as」であることを示すために、asと表記する

JUnitの.isEqualToIgnoreingCase("kotlin")

JUnitで大文字小文字を無視する

assertThat("KOTLIN")
    .isEqualToIgnoreingCase("kotlin")

JUnitの.isNotEqualTo("KOLTIN")

JUnitで等しくない事を示す。

assertThat("KOTLIN")
    .isNotEqualTo("KOLTIN")

JUnitの.startsWith("KO") / .endsWith("IN")

特定の文字列からスタート/終了するか検証する

assertThat("KOTLIN")
    .startsWith("KO")

JUnitの.contains("TL")

特定の文字列が含まれるか検証する

assertThat("KOTLIN")
    .contains("TL")

JUnitの .isInstanceOf(String:class.java)

文字列型であるかどうかを検証する

assertThat("KOTLIN")
    .isInstanceOf(String:class.java)

String型のJUnitのポイント

  1. 次のように、メソッドはチェインすることができる

一個失敗したらその後のアサーションは全て実行されない。

assertThat("KOTLIN")
    .`as`("KOTLINの文字列チェック")
    .isEqualTo("KOTLIN")
    .isEqualToIgnoreingCase("kotlin")
    .isNotEqualTo("KOLTIN")
    .startsWith("KO")
    .endsWith("IN")
    .contains("TL")
    .isInstanceOf(String:class.java)
  1. メソッドチェーンでエラーで躓いてほしくないときは

SoftAssertionsを用いて以下のように書く

SoftAssertions().apply{
    assertThat("KOTLIN")
        .`as`("KOTLINの文字列チェック")
        .isEqualTo("KOTLIN")
        .isEqualToIgnoreingCase("kotlin")
        .isNotEqualTo("KOLTIN")
        .startsWith("KO")
        .endsWith("IN")
        .contains("TL")
        .isInstanceOf(String:class.java)
}.assertAll()

備考

title:JUnitの文字列のアサーション

description:実測値と期待値を比較検証するプロセスのこと。ユニットテスト、ひいてはソフトウェアテストにおいて最も重要な概念。UnitではAssertJのようなアサーションライブラリを用いることで、自然言語に近い直感的な記述が可能になる

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

数値のアサーション

文字列とほとんど同様に、分かりやすいメソッドが用意されている

assertThat(3.1415)
    .isNotZero()
    .isNotNegative()
    .isLessThan(4)
    .isGreaterThanorEqualto(3)
    .isbetween(3.0, 3.2)

JUnitの.isNotZero()

0ではない事を検証する

assertThat(3.1415)
    .isNotZero()

JUnitの.isNotNegative()

マイナスの値ではないことを証明する

assertThat(3.1415)
    .isNotZero()

JUnitの.isLessThan()

ある特定の値より小さいことを証明する

assertThat(3.1415)
    .isLessThan(5)

JUnitの.isGreaterThanorEqualto()

ある値より大きいか等しいかを検証する

assertThat(3.1415)
    .isGreaterThanorEqualto(2)

備考

title:JUnitの文字列のアサーション

description:実測値と期待値を比較検証するプロセスのこと。ユニットテスト、ひいてはソフトウェアテストにおいて最も重要な概念。UnitではAssertJのようなアサーションライブラリを用いることで、自然言語に近い直感的な記述が可能になる

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

よく使うJUnitの配列のアサーション

val sut = arrayOf("red", "green", "blue")
assertThat(sut).hasSize(3)
    .contains("green")
    .containsOnly("blue", "red", "green")
    .containsExactly("red", "green", "blue")
    .doesNotContains("yellow")

JUnitのhasSize(3)

サイズが合っているかを確認する

val sut = arrayOf("red", "green", "blue")
assertThat(sut)
    .hasSize(3)

JUnitの.contains("green")

文字列が含まれているかどうかを確認する

val sut = arrayOf("red", "green", "blue")
assertThat(sut)
    .contains("green")

JUnitの.containsOnly("blue", "red", "green")

同じ要素が含まれているか確認する。

順番は問わない

val sut = arrayOf("red", "green", "blue")
assertThat(sut)
    .containsOnly("blue", "red", "green")

JUnitの.containsExactly("blue", "red", "green")

配列の要素が完全一致することを検証する。

順番まで一致しなければならない

val sut = arrayOf("red", "green", "blue")
assertThat(sut)
    .containsExactly("red", "green", "blue")

JUnitの.doesNotContains

文字列が含まれるかどうかを確認する

val sut = arrayOf("red", "green", "blue")
assertThat(sut)
    .doesNotContains("yellow")

備考

title:JUnitの文字列のアサーション

description:実測値と期待値を比較検証するプロセスのこと。ユニットテスト、ひいてはソフトウェアテストにおいて最も重要な概念。UnitではAssertJのようなアサーションライブラリを用いることで、自然言語に近い直感的な記述が可能になる

img:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmucaKbgWgJTHfHMMdUFAziBpyeB-GNVTnrQrRHJ46re_871oNfxeUkcxdp5RNQaalwc8&usqp=CAU

category_script:True

テストランナーのメリット

  1. テストの実行に時間がかかりすぎる問題

ユニットテストは繰り返し実行するため、一回当たりの時間がかかるのは全体の進捗に関わる。

  1. テストコードがごちゃごちゃになり、可読性を損なう問題

テストコードも管理が必要。

読みづらさは管理しずらさに直結し、管理されていないテストになる。

テストの実行を制御する仕組みが必要。→テストランナー

テストをどのように実行するか?をコントロールする。

Suiteテストランナーで複数のクラスをまとめて実行する

Suiteテストランナーを使って指定した複数のクラスをまとめて実行する

基本的にテストの実行はテストメソッドやテストクラス単位だが、以下のように書くとFooTestとBarTestをまとめて実行できる

@RunWith(Suite::class)
@Suite.SuiteClasses(fooTest::class, BarTest::class)
class AllTest{}

Junit4を使ったテストランナー

@RunWith(JUnit4::class)
class Example {
    @Test
    fun example1() {}
}

JUnit4テストランナーはデフォルトで設定されているテストランナー

設定されたクラス内で次の条件を満たすメソッドをテストケースとして実行する

Suiteテスト

テストランナーには実行の制御方法に応じていくつか種類がある

  1. テストコードの実行に時間がかかる(スローテスト問題)

  2. テストコードが御茶ついてしまう

共通部分をまとめるテストランナーが必要。

これを実現するのがEnclosedテストランナー

Enclosedテストランナー

テストケースを構造化して共通部分をまとめるなど、 テストケースを整理するのが、テストランナーEnclosed

テストケースの整理方法として@Beforeアノテーションがあった

これをBeforeアノテーションを使って初期化しようとすれば以下のようになる。 setUpの中でインスタンス化したので、他のメソッドでインスタンス化する必要はなくなる。

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {
    lateinit var sui: Calculator

    @Before
    fun setUp () {
        sut = Calculator()
    }

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun dividewoで3を2で割った結果を取得できる() {
        val actual = sut.divide(6,2)
        val expected = 3
        assetThat(actual).isEqualTo(expected)
    }

    @Test
    fun divide() {
        val actual = sut.divide(3,2)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

テストの実行・成功に必要なデータなどの前提条件を フィクスチャ(コンテキスト)という

フィクスチャを用意することをセットアップと呼び、テストメソッドごとにフィクスチャをセットアップするやり方をインラインセットアップと呼ぶ

シンプルなフィクスチャであればインラインセットアップで良い。

@Beforeアノテーションがついたセットアップメソッドによってフィクスチャのセットアップを行うメソッドをセットアップメソッドという

コンストラクタで初期化する場合のJUnitの場合

たとえば次のクラスはコンストラクタで初期値を使っている。

class Calculator(val n: Int = 1 ){
    fun multiply(x: Int, y:Int): Int {
        return n*x*y;
    }
}

このように、コンストラクタに初期値が入っている場合、コンストラクタが共通部分に入らないのでスッキリしない、冗長なコードになってしまう。

だが、コンストラクタの初期値が

  • 1の場合と

  • 2の場合で

テストしたい場合、 共通部分として作るのが難しくなる。

...
import org.assertj.core.api.Assertions.*
...

class CalculatorTest {
    lateinit var sui: Calculator

    @Before
    fun setUp () {
        sut = Calculator()
    }

    @Test
    fun multiplyで掛け算の結果が取得できる() {
        val actual = sut.multiply(x:2, y:3)
        val expected = 3
        asserThat(actual).isEauelTo(expected)
    }

    @Test
    fun dividewoで3を2で割った結果を取得できる() {
        val actual = sut.divide(6,2)
        val expected = 3
        assetThat(actual).isEqualTo(expected)
    }

    @Test
    fun divide() {
        val actual = sut.divide(3,2)
        val expected = 1.5
        assetThat(actual).isEqualTo(expected)
    }
}

このような場合にセットアップの共通処理をまとめつつ、多様性を保つことができる上手にまとめることができる仕組みをEnclosedと呼ぶ

...
import org.assertj.core.api.Assertions.*
...

@RunWith(Enclosed::class)
class CalculatorTest {
    class 初期値が1 {
        lateinit var sut: Calculator

        @Before
        fun setUp(){
            sut = Calculator(1)
        }

        @Test
        fun multiplyで掛け算の結果が取得できる() {
            val actual = sut.multiply(x:2, y:3)
            val expected = 3
            asserThat(actual).isEauelTo(expected)
        }

        @Test
        fun multiplyで掛け算の結果が取得できる() {
            val actual = sut.multiply(x:2, y:3)
            val expected = 3
            asserThat(actual).isEauelTo(expected)
        }
    }

    class 初期値が2 {
        lateinit var sut: Calculator

        @Before
        fun setUp(){
            sut = Calculator(2)
        }

        @Test
        fun multiplyで掛け算の結果が取得できる() {
            val actual = sut.multiply(x:2, y:3)
            val expected = 6
            asserThat(actual).isEauelTo(expected)
        }

        @Test
        fun multiplyで掛け算の結果が取得できる() {
            val actual = sut.multiply(x:2, y:3)
            val expected = 6
            asserThat(actual).isEauelTo(expected)
        }
    }

}

ポイントは

  • クラスをネストする形式で多様性を保つことができるということ

  • そのためには@RunWith(Enclosed::class)をつかうことで実装できること

これらのポイントを抑えることで、構造化されたテストの作成とその管理が可能になる。

ちなみのこれを実行した後は、ファイルシステムの構造化された形式のようにスッキリとしたまとまりになる。

Enclosed まとめ

通常、テストクラス内にあるメソッドをテストメソッドとして認識するが、 Enclosedテストランナーを使うと、 ネストされたクラス内のメソッドをテストメソッドとして認識するようになる。

ネストクラスと同じ深さに定義されたメソッドは、テストメソッドとして認識されなくなる事に注意。

テストメソッドをファイル、クラスをフォルダと考えると理解しやすい

ポイントはどのように、どこの軸でまとめたかが分かるような名前にすること

備考

img:https://devlog.arksystems.co.jp/wp-content/uploads/2017/12/junit5.png

title:EnclosedテストランナーをKotlinから使う【JUnit入門】

description:通常、テストクラス内にあるメソッドをテストメソッドとして認識するが、 Enclosedテストランナーを使うと、 ネストされたクラス内のメソッドをテストメソッドとして認識するようになる。ネストクラスと同じ深さに定義されたメソッドは、テストメソッドとして認識されなくなる事に注意。