見出し画像

Now in REALITY Tech #81 JUnit5の導入

こんにちは。
Androidチームの応為です。

サーバサイド、Androidを主軸として開発を行ってきましたが、この6月にAndroidチームにJOINしました。

REALITYにJOINする前はサーバサイドエンジニアやテックリードをしていましたが、その間にCoroutineやJetpackComposeが主流となり新しい技術に追いつくのに必死です。

実は上に挙げたCoroutineやComposeだけでなく、JUnitも4から5にメジャーバージョンアップデートがされているのはご存知でしょうか?

JUnit4は最終バージョンが2021年にリリースされた4.13.2が最終となっておりそれ以降新しいバージョンがリリースされていません。
これから導入を検討しているといった方はJUnit5を入れていくのがよさそうです。
また、JUnit4を既に入れている場合でも4と5を同時に動かせるようにサポートもされていて、書き方も大きく変わるわけではないため徐々にシフトしていくといったことも可能です。

今回はJUnit4を使ったことがあるけどJUnit5を検討している方や、これからユニットテストを書いていこうかなと思っている方向けに、JUnit5の基本的な記述を紹介したいと思います。


JUnitで何ができるの?

作成したプログラムに対して単体テストを機械的に行うことができるようになります。
CI環境で自動的にテストが動くように設定をしておけば、不意に紛れ込んだバグなどの早期発見に繋がるため運用の継続に貢献してくれます。
また、テスト対象としている実装部分において条件分岐や処理結果を把握する必要があるため、どのような実装がされているか内容の理解に役立ちます。

今回は、String#toInt() のテストと、テストをする前後にできることを解説していきます。

どうやって記述するの?

String#toInt()をテストするときには何をチェックできればいいでしょうか?
基本的に単体テストは正常系と異常系を確認しておきたいです。

正常系だと"10"を数値に変換すると、10の値が返却され、
異常系だと "not number"を数値に変換しようとするとNumberFormatExceptionがスローされるのが期待値となります。

実行したいテストはメソッドごとに分けて@Testをつけることで実行対象とすることができます。

値が指定のものと一致していることを検証する場合には、Assertions.assertEqualsを使うことで値が不一致となったケースにエラーをスローさせることができます。

また、指定の例外がスローされることを検証される場合には、assertThrows<T> を使うことで例外がスローされなかったり、指定した例外以外の例外がスローされた場合にエラーをスローさせることができます。

class JUnit5Test {
    @Test
    fun `10をパース`() {
        Assertions.assertEquals(10, "10".toInt())
    }

    @Test
    fun `パース失敗`() {
        assertThrows<NumberFormatException> { "not number".toInt() }
    }
}


テスト前後の処理

JUnitはテストの前後に特定の処理ができるように設定することができます。
@BeforeAll@AfterAllを指定することで、テストクラスが実行される前後の処理を記述することができます。

また、@BeforeEach@AfterEachを指定することで各テストメソッドの前後に実行させたい処理を記述することができます。


@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class JUnit5Test {
    @BeforeAll
    fun beforeAll() {
        println("beforeAll")
    }

    @BeforeEach
    fun beforeEach(){
        println("beforeEach")
    }

    @AfterAll
    fun afterAll() {
        println("afterAll")
    }

    @AfterEach
    fun afterEach() {
        println("afterEach")
    }

    @Test
    fun `10をパース`() {
        println("10をパース")
        Assertions.assertEquals(10, "10".toInt())
    }

    @Test
    fun `パース失敗`() {
        println("パース失敗")
        assertThrows<NumberFormatException> { "not number".toInt() }
    }
}


上記のテストを実行すると、以下のような順で実行がされます。

  1. beforeAll

  2. beforeEach

  3. パース失敗

  4. afterEach

  5. beforeEach

  6. 10をパース

  7. afterEach

  8. afterAll

※ コード上の記述順序とテストの実行順序は必ずしも一致するわけではありません

BeforeXXX/AfterXXXは共通化したい

BeforeXXXやAfterXXXに書く内容はSharedPreferenceやDBの操作、モックインスタンスのDIといったテストする環境を整えたり初期化したりすることが主な処理となってきます。
そのため、テストクラスごとに記述を書いていると煩雑になっていくため共通化できる処理はまとめてしまいたいです。
そんなときに便利なのがExtension@ExtendWithです。

ExtensionはInterfaceですが、それを継承したBeforeAllCallbackやAfterAllCallbackなど、各種テストアノテーションと同じ名前をついたCallBackインターフェースが用意されています。

ExtensionをImplementしたクラスを、@ExtendWithに引数として渡すことでBeforeAllやBeforeEachなどを毎回書かなくても実行してくれるようになります。

前回と同じ出力になるように書いたコードがこちらです。
TestExtensionというクラスを用意ししてExtendWithに渡しています。
またExtendWithに渡すExtensionクラスはarrayで渡すことができるのでDB初期化用Extension、SharedPreference初期化用Extensionと目的をわけて用意することでテストに合わせた組み合わせを渡せるようになります。

@ExtendWith(TextExtension::class)
class JUnit5Test {
    @Test
    fun `10をパース`() {
        println("10をパース")
        Assertions.assertEquals(10, "10".toInt())
    }

    @Test
    fun `パース失敗`() {
        println("パース失敗")
        assertThrows<NumberFormatException> { "not number".toInt() }
    }
}

class TextExtension : BeforeAllCallback,
    BeforeEachCallback,
    AfterAllCallback,
    AfterEachCallback {

    override fun beforeAll(p0: ExtensionContext?) {
        println("beforeAll")
    }

    override fun beforeEach(p0: ExtensionContext?) {
        println("beforeEach")
    }

    override fun afterAll(p0: ExtensionContext?) {
        println("afterAll")
    }

    override fun afterEach(p0: ExtensionContext?) {
        println("afterEach")
    }
}


最後に

今回は基本的な部分を紹介しましたがいかがでしょうか?
シンプルなKotlinテストといった内容でしたが、もちろんDIするインスタンスをモックインスタンスに変えてみたり、Androidの機能をテストできるようなものも用意されています。
まだ導入していないけど、これからやろうかな等思っている方がいれば簡単なところから導入されてみてはいかがでしょうか?