新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。もう新人じゃないかも…

学習記録:Scala関数型デザイン 第8章

第8章:プロパティベースのテスト

本章では,プロパティベースのテストを可能にするためのシンプルながら強力なライブラリに取り組む。
こうしたライブラリの主な目的は,プログラムの振る舞いに関する仕様をテストケースの作成から切り離すことにある。
プログラマはプログラムの振る舞いを指定し,テストケースの制約を高いレベルで定義することに専念する。
それにより,こうした制約を満たすテストケースがフレームワークによって自動的に生成され,テストが実行される。

速習:プロパティベースのテスト

例として,ScalaCheckというプロパティベースのテストライブラリのプロパティを見てみる。

// 0~100の整数リストのジェネレータ
val intList = Gen.listOf(Gen.choose(0, 100))
// List.reverseメソッドの振る舞いを指定するプロパティ
val prop =
  forAll(intList)(ns => ns.reverse.reverse == ns) && // リストを2回リバースすると元のリストに戻ることを確認
  forAll(intList)(ns => ns.headOption == ns.reverse.lastOption)  // リストをリバースすると最初の要素が最後の要素になることを確認
// 明らかに偽であるプロパティ
val failingProp = forAll(intList)(ns => ns.reverse == ns)

これらのプロパティをチェックする方法は以下のようになる。

scala> prop.check
+ OK, passed 100 tests.

scala> failingProp.check
! Falsified after 2 passed tests.
> ARG_0: List("0", "1")
> ARG_0_ORIGINAL: List("71", "86")

この時のintListは,List[Int]ではなくList[Int]型のテストデータを生成する方法を知っているGen[List[Int]]になる。
このジェネレータからサンプルを生成することができ,0~100の乱数が含まれた様々なリストが生成される。

forAll関数は,Gen[A]型のジェネレータをA => Boolean型の述語と組み合わせることでプロパティを作成する。
プロパティは,ジェネレータによって生成されたすべての値が述語を満たさなければならないことを宣言する。

テストの流れは以下のようになっている。
prop.checkを呼び出すと,ScalaCheckがList[Int]型の値をランダムに生成し,
指定された述語(ns.reverse.reverse == ns && ns.headOption == ns.reverse.lastOption)を反証するケースを見つけ出そうとする。
先の出力では,ScalaCheckがList[Int]型のテストケースを100個生成し,それらが全て述語を満たしたことが示されている。
もちろんプロパティが失敗するケースもあり,failingProp.checkの出力では一部で述語のテストが偽と判定されたことが示されている。

Exercise 8.1
  • sum:List[Int] => Int関数の実装を指定するプロパティを考え出せ。出発点として以下を参考にせよ。
    • リストをリバースしてから合計すると,元のリバースしていないリストを合計した場合と同じ結果になるはずである。
    • リストのすべての要素が同じ値である場合,合計はどうなるか。
    • 他のプログラムを思いつけるか。

以下のような感じになるのではないか。

val prop =
  forAll(intList)(ns => ns.sum == ns.reverse.sum) && // リストの合計とリバースしたリストの合計は等しい
  forAll(sameNumIntList)(ns => ns.sum == (ns.headOption * ns.length))  // 全要素が同じ値のリストなら合計=値*リストの長さ
  forAll(increaseByOneIntList)(ns => ns.sum == ((ns.headOption + ns.lastOption) * ns.length / 2)) // 1ずつ増えるリストなら...
Exercise 8.2
  • List[Int]の最大値を検出する関数を指定するプロパティはどのようなものになるか。

以下のようなプロパティを考えた。

  • 値が昇順に並んでいるリストに対しては,リストの最後の値が得られる。その逆も然り。
  • 全要素が同じ値xのリストに対しては,値xが得られる。
  • 要素が2つの値x, yのリストに対しては,大きい方の値が得られる。
  • Int.MaxValueが含まれるリストに対しては,Int.MaxValueが得られる。

速習:プロパティベースのテスト(承前

プロパティベースのテストライブラリには,便利な関数が他にも搭載されている。

  • テストのケースの最小化
    • デバッグ作業を容易にするため,失敗するテストでは最も小さいテストケースが検出されるまでサイズを縮めていく
    • 例えば,サイズ10のリストでプロパティが失敗した場合,更に小さいリストを試してテストに失敗する最も小さいリストを報告
  • 包括的なテストケースの生成
    • Gen[A]によって生成される値の集まりをドメインと呼ぶ。
    • ドメインが十分に小さい場合(100よりも小さい全ての偶数など),それらの値を全てテストすることが可能である。
    • ドメイン内の全ての値に対しプロパティが有効であれば,反証が無いのではなく実証されたことになる。

ScalaCheckはプロパティベースのテストライブラリの1つに過ぎない。
ScalaCheckに何か問題があるわけではないが,本章ではカスタムライブラリを一から作成することにする。