🐣

Android kotlinでUnit Testを体験してみた

9 min read

前回の記事はこちら

3 実際にUnit Testを書いてみる

今回、基本的に参考にするのは以下の2つの記事。

  1. Android Unit Test Tutorial – Writing Your First Unit Test / Belal Khan
    https://www.simplifiedcoding.net/android-unit-test-tutorial/
  2. ローカル単体テストを作成する / Android
    https://developer.android.com/training/testing/unit-testing/local-unit-tests?hl=ja

しかし、現在は非推奨のassertThatが使われているので、assertEqualsに置き換えて書いていきます。日本語ではこちらの記事[1]が参考になりそうですが、robolectricを使っているので、今回は紹介のみです。

3.1 とりあえずサンプルのアプリを作る

今回はUnit Testの勉強なので、とりあえずアプリの大枠を作ります。このアプリのアーキテクチャについては、奥澤さんの本[2]を参考にしました。
作ったアプリはこんな感じです。最初のページで文字をText Fieldそれぞれに入力したら、それをくっつけて表示するというものです。Githubにソースコードを置いています。

ソースコードはこちらに置いています(以下のリンクでも飛べます)。
https://github.com/KASHIHARAAkira/android-test-drill
mainブランチはunit testのコードがないもの、unit-testブランチはunit-testのコードを入れたものになります。

sample app behavior

3.2 今回Unit Testの対象となるコード

今回のテストの対象はconcatStrというmethodで引数に入れた2つの文字列を結合するという、本当に簡単なコードです。

VirtualModelImpl.kt
package email.example.praticaltest.model

import javax.inject.Inject

class VirtualModelImpl @Inject constructor(): VirtualModel {
    override suspend fun concatStr(str1: String, str2: String): String {
        return str1 + str2
    }
}
method(メソッド)とfunction(関数)の違い

気になったので調べてみました。恐らく、引数を代入して戻り値を返すという点では、さほど違いがないように思います。今回の例だと関数でいいじゃないかとなります。しかし、メソッドはクラス内に含まれるデータを操作したり、関数は明示的に呼び出されるのに対し、呼び出されるときに暗黙的にオブジェクトに関連する名前によって呼び出されたりするといった点で異なります[3]

3.3 Unit Testのファイルを作る

testフォルダにすでにあるExampleUnitTest.ktを編集しても作ることが出来ますが、Android公式ページ「アプリをテストする」[4]には、新しくテスト用のファイルを作る方法が書いてありますので、そのとおりに作成します。詳細の作り方は割愛しますが、端的に言うとconstructor()の上でCtrl+Shift+T キー(⇧⌘T)を押すことで作成することが出来ます。
出来たファイルはtest/modelの中に作成されました。
Test file position

3.4 Unit Testのコード

実際に書いたUnit Testのコードを貼り付けます。今回、対象のmethodがsuspendなので、このテストの書き方については、こちらの記事[5][6]で詳しく書かれています。

VirtualModelImplTest.kt
package email.example.praticaltest.model

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test

class VirtualModelImplTest {
    private val scope = CoroutineScope(Dispatchers.Default)

    @Test
    fun whenInputIsValid() {
        val virtualModelImpl = VirtualModelImpl()
        val str1 = "Hi!"
        val str2 = "I'm Akira."
        val result = runBlocking { virtualModelImpl.concatStr(str1, str2) }
        assertEquals(str1+str2, result)
    }
}

assertThatの代わりにassertEqualsを使用しています。
とりあえず通るかどうか、この簡単な例で試してみます。VirtualModelImplTestの上(コード上であればどこでも良い)で右クリックをし、「Run "VirtualModelImplTest"」を選択すれば動きます。実行結果は以下の通り。

exec result

ちゃんと動いていそうなので、テストのパターンを考えたいと思います。

3.5 テストパターンを考える

とても、がっちり当てはまる記事[7]を見つけたので、こちらを参考にします。

3.5.1 仕様の確認

今回は、入力された2つの文字列を結合して出力するというのが基本的な仕様になります。また、nullと文字列は許すけど、それ以外は許しません。という方針でいきたいと思います。
じゃあ入力がnullのときは?数字など他の型のときは?などがありますね、では入力と出力それぞれの出力できるパターンについて考えます。ところが、今回入力はTextFieldを使いますので、文字列が来るという前提でいきたいと思います。

表1: 入力、出力パターン

入力 出力
文字列,null 入力を連結した文字列、エラー文字

としたいと思います(本当はnullではなく""となると思うのですが、記事の書きやすさ的に、こういうふうにしました)。

3.5.2 入出力パターンをテスト項目として整理する

こちらの記事[7:1]ではスプレッドシートでデシジョンテーブルを作っていたのですが、少ないのでmarkdownでガリゴリ書きます。

また、nullと文字列は許すけど、それ以外は許しません。という方針でいきたいと思います。

と書きましたので、以下のようになりました。

表2: テスト項目

Case 入力1(str1) 入力2(str2) 出力
1 文字列 文字列 入力1+入力2
2 文字列 null 入力1
3 null 文字列 入力2
4 null null "文字列が入力されていません"

今回、実際のアプリケーションではTextFieldを使っているので、

var text by remember { mutableStateOf("Hello") }

のtextを更新する形で、入力されるのは文字列として解釈されると思うので、文字列以外の入力は外しています。

3.6 テスト項目を元に実装する。

では、表2を元に実装していきます。
実装し始めると、既にnullを許していないと怒られるので、nullを許す形に書き換えます。

VirtualModelImpl.kt(version2)
package email.example.praticaltest.repository

import email.example.praticaltest.model.VirtualModel
import javax.inject.Inject

class UserRepositoryImpl @Inject constructor(
    val virtualModel: VirtualModel,
): UserRepository {
    override suspend fun concatStr(str1: String?, str2: String?): String {
        return virtualModel.concatStr(str1 = str1, str2 = str2)
    }
}

その上で出来たテストコードはこちら

VirtualModelImplTest.kt(version2)
package email.example.praticaltest.model

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test

class VirtualModelImplTest {
    private val scope = CoroutineScope(Dispatchers.Default)

    //1
    @Test
    fun whenInputIsValid() {
        val virtualModelImpl = VirtualModelImpl()
        val str1 = "Hi!"
        val str2 = "I'm Akira."
        val result = runBlocking { virtualModelImpl.concatStr(str1, str2) }
        assertEquals(str1+str2, result)
    }

    //2
    @Test
    fun whenInput1only() {
        val virtualModelImpl = VirtualModelImpl()
        val str1 = "Hi!"
        val str2 = null
        val result = runBlocking { virtualModelImpl.concatStr(str1, str2) }
        assertEquals(str1, result)
    }

    //3
    @Test
    fun whenInput2only() {
        val virtualModelImpl = VirtualModelImpl()
	
        val str1 = null
        val str2 = "I'm Akira."
        val result = runBlocking { virtualModelImpl.concatStr(str1, str2) }
        assertEquals(str2, result)
    }

    //4
    @Test
    fun whenNull() {
        val virtualModelImpl = VirtualModelImpl()
        val str1 = null
        val str2 = null
        val result = runBlocking { virtualModelImpl.concatStr(str1, str2) }
        assertEquals("文字列が入力されていません", result)
    }
}

3.6.1 動かしてみる

当たり前ですがコケます。コケた結果が以下です。

expected:<Hi![]> but was:<Hi![null]>
Expected :Hi!
Actual   :Hi!null
<Click to see difference>

expected:<[]I'm Akira.> but was:<[null]I'm Akira.>
Expected :I'm Akira.
Actual   :nullI'm Akira.
<Click to see difference>

expected:<[文字列が入力されていません]> but was:<[nullnull]>
Expected :文字列が入力されていません
Actual   :nullnull
<Click to see difference>

3.6.2 修正する

良さげに修正します。

VirtualModelImpl.kt
package email.example.praticaltest.model

import javax.inject.Inject

class VirtualModelImpl @Inject constructor(): VirtualModel {
    override suspend fun concatStr(str1: String?, str2: String?): String {
        if (str1 === null && str2 != null) {
            return str2
        } else if (str1 != null && str2 === null) {
            return str1
        } else if (str1 === null && str2 === null) {
            return "文字列が入力されていません"
        }
        return str1 + str2
    }
}

3.6.3 修正したmethodをUnit Testする

先程書いたテストのまま実行すると、無事通りました。

まとめと感想

ひとまず、簡単なUnit Testは実装できたと思います。今後、UIテストもやってみたいなと思いました。
今回、実装していて躓いたのはsuspendをうまく呼び出せなかったり、assertEqualsで本当に良かったのかというのを調べるのに時間がかかりました。

脚注
  1. AndroidでのUnitTestざっくり入門 / nozaki-sankosc https://qiita.com/nozaki-sankosc/items/538fe9b2a13aa7403df6 ↩︎

  2. 奥澤 俊樹, "Jetpack Compose による Android MVVM アーキテクチャ入門", 株式会社インプレス R&D, 2021, ISBN978-4-295-60053-4 ↩︎

  3. What's the difference between a method and a function? / Andrew Edgecombe on stackoverflow https://stackoverflow.com/questions/155609/whats-the-difference-between-a-method-and-a-function ↩︎

  4. アプリをテストする / Android https://developer.android.com/studio/test ↩︎

  5. 【Kotlin】suspend functionとテストの書き方 / Mori Atsushi https://at-sushi.work/blog/30/ ↩︎

  6. CoroutinesのUnit Testメモ / Kenji Abe https://star-zero.medium.com/coroutines%E3%81%AEunit-test%E3%83%A1%E3%83%A2-b17806a1d252 ↩︎

  7. テストコードの書き方はわかったけどテストケースってどうやって挙げたらいいの? / tan3_sugarless on Qiita https://qiita.com/tan3_sugarless/items/7bd89c92bfaac1e852e0 ↩︎ ↩︎

Discussion

ログインするとコメントできます