Androidのユニットテストで利用するダミーデータをJSONで定義してGsonでデータクラスに変換する

# 背景
私はこれまでテストコードを書く際には、テスト用のダミーデータをコード上にベタ書きしていました。
例えば下記のようなクラスのテストを書きたい場合はこんな感じに実装していました。

```kotlin:Person.kt
data class Person(
    val firstName: String,
    val lastName: String
)
```

```kotlin:PersonTest.kt
class PersonTest {

    private lateinit var testData: Person

    @Before
    fun setUp() {
        // ダミーデータをベタ書きしてテストデータのインスタンスを生成
        testData = Person("hoge", "fuga")
    }    

    @Test
    fun getFirstName() {
        assertEquals("hoge", testData.firstName)
    }

    @Test
    fun getLastName() {
        assertEquals("fuga", testData.lastName)
    }
}
```

今回のように簡単なクラスであれば問題ありませんが、フィールドが多かったり中身がリストだった場合、または色々なテストケースで値を使いまわしたい場合などにこのやり方だと不便でした。
なので、この不便を解消するためにダミーデータをJSONとして定義しておき、そのデータからGsonを用いてテストデータのインスタンスを生成することに挑戦してみました。

# 手順
## ダミーデータJSONの作成
引き続き `Person.kt`を使います。
まずはじめにダミーデータとして使いたい値をJSONに定義します。
この時にJSONはresourcesディレクトリ内に配置します。

```JSON:app/src/test/resources/dummy_data_person.json
{
    "firstName": "hoge",
    "lastName": "fuga"
}
```

## ダミーデータJSONからテストデータのインスタンスを作成
先ほどのテストコード`PersonTest.kt`の`@Before`で行なっていたテストデータのインスタンス生成処理をJSONからのインスタンス生成処理に置き換えます。

```kotlin:PersonTest.kt
class PersonTest {

    private lateinit var testData: Person

    @Before
    fun setUp() {
        val inputStream = ClassLoader.getSystemResourceAsStream(PATH_DUMMY_DATA)
        val inputStreamReader = InputStreamReader(inputStream)
        try {
            testData = Gson().fromJson(inputStreamReader, Person::class.java)
        } catch (e: JsonSyntaxException) {
            e.printStackTrace()
        } catch (e: JsonIOException) {
            e.printStackTrace()
        } finally {
            inputStreamReader.close()
        }
    }

    // テストメソッドは省略...

    companion object {
        /** resourcesディレクトリ配下のパス */
        private const val PATH_DUMMY_DATA: String = "dummy_data_person.json"
    }
}
```

# おまけ
JSON読み込み処理は共通化しておくと色々なテストコードで使えるので便利です。
（Gsonを用いた場合、配列を定義したJSONをListに変換するのが大変だったのでそれもよしなにやってくれるようにしてみました。）

```kotlin:TestDataCreator.kt
object TestDataCreator {

    inline fun <reified T> fromJson(dummyDataPath: String): T {
        var testData: T? = null

        val inputStream = ClassLoader.getSystemResourceAsStream(dummyDataPath)
        val inputStreamReader = InputStreamReader(inputStream)

        try {
            val type = object : TypeToken<T>() {}.type
            testData = Gson().fromJson(inputStreamReader, type)
        } catch (e: JsonSyntaxException) {
            e.printStackTrace()
        } catch (e: JsonIOException) {
            e.printStackTrace()
        } finally {
            inputStreamReader.close()
        }

        return testData ?: throw Exception("testData is null. It's not initialized.")
    }
```

# やってみて感じたメリット
- API通信などでGsonを用いている場合、データクラスの定義がJSONの形式と一致しているかのJSONパースのテストにもなる。
- ダミーデータを使い回せるので複数箇所でデータを使う場合に楽になる。
- サーバー仕様書などに返却値のサンプルJSONが記載されていた場合にそれをそのまま利用できる。
- ダミーデータの定義が繁雑にならずに分かりやすい。


# まとめ
ダミーデータのベタ書きをやめたことでテストコードとダミーデータの区分けができてスッキリしました。
まだ使い始めたばかりでデメリットが見つけられていませんが、今後も運用してみて何かあれば記載しようと思います。
お読みいただき、ありがとうございました！


# 注意点（2018年10月17日追記）

## プロパティを`by lazy`で初期化できない

### 発生した事象
下記のように`by lazy`を用いてプロパティを定義した場合、該当プロパティが初期化されないことが分かりました。

```kotlin:Person.kt
data class Person(
    val firstName: String,
    val lastName: String,
    private val bloodTypeStr: String
) {
    
    val bloodType: BloodType by lazy {
        BloodType.from(bloodTypeStr)
    }    
}
```

```kotlin:BloodType.kt
enum class BloodType(private val bloodTypeStr: String) {
    A("a"),
    B("b"),
    AB("ab"),
    O("o"),
    UNKNOWN("unknown");
    
    companion object {
        fun from(bloodTypeStr: String): BloodType = 
            values()
            .find { 
                it.bloodTypeStr == bloodTypeStr
            } ?: UNKNOWN
    }
}
```

```JSON:dummy_data_person.json
{
    "firstName": "hoge",
    "lastName": "fuga",
    "bloodTypeStr": "a"
}
```

```kotlin:PersonTest.kt
class PersonTest {

    private lateinit var testData: Person

    @Before
    fun setUp() {
        // おまけの項目で作成したTestDataCreatorを用いています
        testData = TestDataCreator.fromJson(PATH_DUMMY_DATA)
    }    

    @Test
    fun getBloodType() {
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // testData.bloodTypeがNullPointerExceptionになってしまう。
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        assertEquals(BloodType.A, testData.bloodType)
    }

    companion object {
        /** resourcesディレクトリ配下のパス */
        private const val PATH_DUMMY_DATA: String = "dummy_data_person.json"
    }
}
```

### 原因はGsonでのインスタンス生成にありそう
調査したところ、どうやらGsonからのインスタンス生成時のみこの事象が起きるようです。
試しにコンストラクタを用いてPersonのインスタンスを生成してみたところ、`by lazy`で定義したプロパティもきちんと初期化されていました。

```kotlin:PersonTest.kt
class PersonTest {

    private lateinit var testData1: Person
    private lateinit var testData2: Person

    @Before
    fun setUp() {
        // こちらのデータではbloodTypeがNullPointerExceptionに。
        val testData1 = TestDataCreator.fromJson(PATH_DUMMY_DATA)
        // こちらのデータではbloodTypeがBloodType.Aに。
        val testData2 = Person("hoge", "fuga", "a")
    }

    // 以下省略...
}
```

`by lazy`の初期化処理周りに原因がありそうな気がしたので、処理を追うためにPerson.ktをKotlinバイトコードに変換してからJavaコードにデコンパイルしてみました。（一部省略しています。）

```java:Person.decompiled.java
public final class Person {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "bloodType", "getBloodType()Ljp/co/kintetsu/portal/net/model/train/info/BloodType;"))};
   @NotNull
   private final Lazy bloodType$delegate;
   @NotNull
   private final String firstName;
   @NotNull
   private final String lastName;
   private final String bloodTypeStr;

   @NotNull
   public final BloodType getBloodType() {
      Lazy var1 = this.bloodType$delegate;
      KProperty var3 = $$delegatedProperties[0];
      return (BloodType)var1.getValue();
   }

   public Person(@NotNull String firstName, @NotNull String lastName, @NotNull String bloodTypeStr) {
      Intrinsics.checkParameterIsNotNull(firstName, "firstName");
      Intrinsics.checkParameterIsNotNull(lastName, "lastName");
      Intrinsics.checkParameterIsNotNull(bloodTypeStr, "bloodTypeStr");
      super();
      this.firstName = firstName;
      this.lastName = lastName;
      this.bloodTypeStr = bloodTypeStr;
      this.bloodType$delegate = LazyKt.lazy((Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            return this.invoke();
         }

         @NotNull
         public final BloodType invoke() {
            return BloodType.Companion.from(Person.this.bloodTypeStr);
         }
      }));
   }
}

```

デコンパイル後の処理を見てみると`getBloodType()`メソッドで取得する値は`bloodType$delegate`から取得しているようです。
そしてこの`bloodType$delegate`が初期化されるのはコンストラクタ内です。

Gsonの処理が追いきれずここからは推測になってしまいますが、恐らくGsonによるJSONからのオブジェクト作成の処理ではコンストラクタを介してのインスタンス生成にはなっていないのではないかと思われます。
そのため`bloodType$delegate`が初期化されず`null`のままになり、`getBloodType()`を呼び出したタイミングで`NullPointerException`が発生してしまうと考えられます。

対処法はまだ考えられていませんが、検討出来次第再度追記したいと思います。