test
Kotlin
testing

いいテストをするためのテストデータの作り方

この記事は リクルートライフスタイル Advent Calendar 2017 19日目の記事です。

はじめに

ホットペッパービューティーでAndroidアプリの開発をしている @yrhorita です。
この記事では、アプリケーションのテストにおけるデータとその作り方について、普段自分が考えていることを書きます。

テストとは

ここでは、負荷テストのようなアプリケーション全体に対する大きいテストから、ユニットテストのような関数やメソッドなどに対する小さいテストまで、広い範囲を考えてみたいと思います。

いいテストってなんだろう

話が大きくなりそうなので割愛します🙇
ここでは、「いいテストデータは、いいテストをするための一要素」であるということにします。

いいテストデータってなんだろう

いろいろな考え方があると思いますが、「常に本番のデータに近いテストデータが理想」というわけではなく、テストの「目的に応じたデータが用意することが理想」だと自分は思っています。
幾つかのテストを例に考えてみます。

  • 負荷テストは本番っぽいデータがいい
    • データの量だけでなく、質の面でも本番っぽさが大事
    • 雑なデータでシステム負荷が本番っぽくなくなると、テストの意義が薄れる
      • アクセスするテーブルの数やリクエスト/レスポンスのサイズなど
  • UIテストは境界値を持ったデータも使いたい
    • 文字数MAXパターンを見たい、など
    • あまりに不自然な文字列を入れるとテストの意味が薄れることもあるので注意も必要
      • ellipsize/truncateが不自然な挙動になるなど
      • 「不自然なテストデータも自然に表示できるようなロジックを入れる」のは本末転倒
  • ユニットテストは特定の値の境界値しか気にならないことが多い
    • 本番データにはそのようなデータがほとんどないとしても、アプリケーションとして保証する上限値/下限値についてテストしたい

データの作り方

それぞれのテストをやるにあたり、如何にしていいテストデータを用意するかを考えてみます。

本番っぽいデータ

(適宜マスクなどして)本番から持ってくるのが一番手っ取り早いと思います。そしてそれは間違いなく本番っぽいデータです。

しかし、諸々の事情で本番データの持ち出しができなかったり、そもそも新規サービスのときは本番データが存在しなかったりと、本番からデータを持ってくることが困難なケースもあります😇

そんなときは自前でデータ作成を頑張るしかないです💪
上の例のように負荷テストを想定すると、データはDBに用意したいことが多いと思います。幾つかの方法が考えられますが、SQLなどで直接DBに突っ込むよりも、きちんとアプリケーションコードを通過した方が良いと考えています。

DB上の制約では表現しきれていないドメイン知識というのは往々にしてあります。つまり、「DB制約的にはOKだけど、アプリケーションとしてはNGなデータ」というのはあり得ます。
それを踏まえて、アプリケーション(ドメインモデルなど)で不整合が起きないように気を使ってSQLを書くのは、結構しんどいと思います。自動生成なども同様です。

きちんとアプリケーションコードを通過させることで、アプリケーションとして保証するデータを作りやすいです。
ただ、サーバサイドアプリケーションのAPIを叩いてデータを入れる、というところまでやると、もはやそれが負荷テストになってしまいます。
実際のアプリケーションのモデルやRepositoryを利用してテストデータを作成するコードを書くというのが一つの落とし所になりそうです。
(気が向いたらこのあたりの記事も書きたいです)

境界値を持ったデータ

UIテストのためのテストデータはjsonなど、ユニットテストであればモデル/データクラスをインスタンス化するなどになるでしょう。
これらのテストデータでは、気にしない値は気にしないことを明示する方がいいです。dummy, 0 などの「いかにも無視している値」に統一することで、テストでフォーカスする部分を浮き彫りにできます。
自分はよく、気にする値だけ設定できるようなBuilderを作っています。以下に例を示します。

Kotlin
// モデル
data class User(
    val id: Id,
    val name: String,
    val someProperty: SomeType,
    ...

) {

    // このメソッドをテストしたい
    fun hasValidName(): Boolean {
        // some validation
    }
}

// テストデータ生成用のクラス
class UserBuilder {

    // Userモデルのプロパティを宣言し、デフォルト値を適当に持っておく
    private var id: Id = Id()
    private var name: String = ""
    private var someProperty: SomeType = SomeType()
    ...

    // 特定のプロパティを変更できるようにしておく
    fun id(id: Id): UserBuilder {
        this.id = id
        return this
    }

    fun name(name: String): UserBuilder {
        this.name = name
    }

    ...


    // Userモデルを取り出す
    fun build(): User = User(id, name, someProperty, ...)

}

// Usage
class SomeTest {

    @Test
    fun notGoodTest() {
        // どこに関心があるかがわかりづらい
        val user = User(id = Id("0000"), name = "some illegal name", someProperty = ...)

        assertFalse(user.hasValidName())
    }

    @Test
    fun betterTest() {
        // 関心がある値だけ設定
        // (話はズレるが、命名も大事)
        val illegalNameUser = UserBuilder().name("some illegal name").build()

        assertFalse(illegalNameUser.hasValidName())
    }

}

上の例でKotlinを使っておいて言うのもなんですが、 Kotlinのデータクラスであれば、copy() の利用でもっと楽に書けます。

Kotlin
class SomeTest {

    private val dummyUser: User = User(id = Id(), name = "", someProperty = ...)

    @Test
    fun goodTest() {
        val illegalNameUser = dummyUser.copy(name = "some illegal name")

        assertFalse(illegalNameUser.hasValidName())
    }

}

おわりに

いいテストがあるとエンジニアの心理的安全性は高まります。
いいテストと共に、安心して楽しいクリスマスを迎えたいですね🎄
おしまい。