NPのAndroidで使われているUnitTestについて

こんにちは。NewsPicks Androidアプリエンジニアの sefwgweo です。
今回はタイトル通りNPのAndroidで使われているUnitTestについて話をします。

要件

正解データはJsonファイルとして用意されるので、それを用いたアサーションを行うこと。

苦労ポイント

Json同士を比較するために、Javaの世界ではいくつかライブラリがありましたがAndroid内で使おうとすると色々問題が出たため、初期の頃はケースによってはMapに変換してアサーションしていました。
Map変換していた理由としては、テストケースによって無視する必要がある要素があったためとなります。

上記について、コードとともに見ていきます。

まずは、ライブラリを使わずにどうやってアサーションをしているかみていきます。

OriginalJsonCompareTest.kt

  1. assets配下に保存されたJsonを読み込み、GsonでEvent1というオブジェクトに変換したものを正解データとして格納します
  2. 検査対象のデータは取り急ぎmapで生成します
  3. 生成された検査対象のデータと、Jsonがオブジェクトに変換されたデータをヘルパーメソッドに渡してアサーションします
@RunWith(AndroidJUnit4::class)
class OriginalJsonCompareTest {
    @Test
    fun event1Test2() {
        // assets配下に保存されたJsonを読み込みEvent1オブジェクトに変換
        val expectedObj = decodeJson(loadJsonFromAssets(event1JsonPath), Event1::class.java) as Event1

        // 検査対象のデータは取り急ぎmapで生成
        val actualObjTest1 = mapOf("keyCode" to "1234-5678")
        val actualObjTest2 = mapOf(
                "id" to "5759348",
                "count" to "417355",
                "name" to "TOP"
            )

        // アサーション実行
        assertion(expectedObj, listOf(actualObjTest1, actualObjTest2))
    }
}

// Jsonをオブジェクト変換
fun decodeJson(jsonString: String, clazz: Class<out ExpectedObject>): ExpectedObject {
    return Gson().fromJson(jsonString, clazz)
}

// assetsからJson読み込み
fun loadJsonFromAssets(path: String): String {
    return appContext.assets.open(path).use {
        it.reader().readText()
    }
}

// アサーション
inline fun <reified T : ExpectedObject> assertion(
    expectedData: T,
    actualValues: List<Map<String, String?>>
) {
    when (expectedData) {
        is Event1 -> {
            // DataClassをMutableなMapにする
            val map1 = expectedData.TEST1[0].asMap().toMutableMap()
            val map2 = expectedData.TEST2[0].asMap().toMap()

            // Info1でNullableで、本テストでは削除対象なKey
            val nullableKeys = listOf("total", "date")
            nullableKeys.map {
                if (map1[it] == null) {
                    map1.remove(it)
                }
            }
            
            TestCase.assertEquals(map1, actualValues[0])
            TestCase.assertEquals(map2, actualValues[1])
        }
        else -> {
        }
    }
}

// DataクラスをMapにするための処理(リフレクションを用いています)
inline fun <reified T : Any> T.asMap(): Map<String, Any?> {
    val props = T::class.memberProperties.associateBy { it.name }
    return props.keys.associateWith { props[it]?.get(this) }
}

上記がコード概要になります。
ポイントとしては、DataClassをMap変換する時にリフレクションが必要なため
implementation 'org.jetbrains.kotlin:kotlin-reflect’
を読み込んでいます。

次にJsonAssertというライブラリを使った場合を展開します。

UseLibraryJsonCompareTest.kt

assets配下に保存されたJsonを読み込み、検査対象のJsonとアサーションするのみとなります。

@RunWith(AndroidJUnit4::class)
class UseLibraryJsonCompareTest {
    @Test
    fun event1Test1() {
        val expected = loadJsonFromAssets(event1JsonPath)
        val actual = "{ \"TEST1\": [{\"keyCode\": \"1234-5678\"}],  " +
            "\"TEST2\": [{\"id\": \"5759348\",\"count\": \"417355\",\"name\": \"TOP\"}]}"

        JSONAssert.assertEquals(expected, actual, JSONCompareMode.LENIENT)
    }
}

上記がコード概要になります。ポイントとしては、該当ライブラリを入れるため
androidTestImplementation "org.skyscreamer:jsonassert:1.5.1”
を読み込んでいます。

JSONCompareModeにより、厳密にも緩めにも判定可能かつ、拡張して独自比較することも可能なためMapに変換が不要になりました。
使い勝手的にはこちらのJsonUnitというライブラリのほうが良さそうだったのですが、うまく動作せず諦めました。

以上がコードによる全容です。
もしもJsonを用いたUnitTestが必要で同様の問題を抱えている場合の1つの解として参考になれば幸いです.。
以下に今回解説したもののリポジトリを載せておきます。
https://github.com/sefwgweo/UnitTest

まとめ

今回はUnitTestについて取り上げてみましたがいかがだったでしょうか
こちらの記事が同様の機能をこれから開発する開発者様の助けになり、また弊社のサービスに興味を持っていただけるきっかけになりましたら幸いです

おわりに

ユーザベースではエンジニアを募集しています。ご興味ある方いらっしゃいましたらこちらからぜひご応募いただければと思います!

Page top