こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 4 回です。Weekly Report については、第 1 回の記事を参照してください。
ドア破壊テスト
以下の IntAdder
は、add
で与えられた整数値の合計を保持します。flush
が呼び出されると、現在の合計値を 0 にリセットし、「リセットされる直前の合計値」を戻り値として返します。
val adder = IntAdder()
adder.add(1)
adder.add(2)
println(adder.flush()) // Shows 3
adder.add(100)
adder.add(200)
println(adder.flush()) // Shows 300
IntAdder
の実装は以下のようになっているとします。
class IntAdder {
private var currentSum: Int = 0
fun add(value: Int) {
currentSum += value
}
fun flush(): Int {
val result = currentSum
currentSum = 0
return result
}
}
ここまでのコードは、仕様を実現するために必要十分であり、特に問題はないと仮定します。
次に、ある開発者が IntAdder
のユニットテストを作ろうとして、currentSum
の可視性を private
から internal
に変えたとします。このように、テストのためだけに可視性を変えたときは、Guava ライブラリの @VisibleForTesting
などを使って「プロダクションコード用の可視性ではない」ことを明示できます。また、テストのための可視性は、Lint ツール等によって検証することもできます。
class IntAdder {
var currentSum: Int = 0
@VisibleForTesting internal get
private set
この currentSum
を使って、以下のような「現在の合計値が正しいか」を検証するユニットテストが作成されました。
// Unit test code
adder.add(100)
assertEquals(100, adder.currentSum)
adder.add(200)
assertEquals(300, adder.currentSum)
assertEquals(300, adder.flush())
assertEquals(0, adder.currentSum)
このユニットテストのコードに問題点はありますか?
空いた窓だけでテストする
ユニットテストでは、内部の詳細な働きよりも、観測できる振る舞いが仕様と一致するか をテストする方がより好ましいです。言い換えると、ユニットテストで確認するべきことは主に以下の 2 点になります。
- 関数の戻り値や例外
- 外部から与えられるオブジェクトとの相互作用 (実引数として与えられたオブジェクト、コンストラクタ引数として与えられたオブジェクト、インジェクトされたオブジェクトなど)
戻り値のテスト
先程の IntAdder
の実装では、現在の合計値を数値として保持していました。しかし、これは仕様の変更によって変わることもありえます。例えば、「値の変更履歴」の取得が必要になった場合、var currentSum: Int
というプロパティは val valueHistory: MutableList<Int>
という形に変わるかもしれません。その場合でも、add
と flush
に対するテストは変わらず有効であるべきです。しかし実際には、currentSum
というプロパティ自体が変わったことにより、既存のテストはコンパイルできなくなってしまいます。
この問題を解決するためには、currentSum
の値を覗き見るテストを書くのではなく、関数の戻り値をテストするのが良いでしょう。
// Unit test code
adder.add(100)
adder.add(200)
assertEquals(300, adder.flush())
assertEquals(0, adder.flush())