テストコードを楽に書きたい

## この記事はなに

Property Based Testingは何となく難しくて普通のアプリケーション開発におけるテストコードには使用しないイメージかも知れないが、単にテストデータ自動生成器くらいに捉えてカジュアルに使ってみてはいかがでしょうか？という紹介です。
言語にはScalaを用いますが、他言語でも探せばライブラリやフレームワークは見つかると思います。

## UnitTestって面倒くさいですよね

JUnitとかScalaTestで書いているテストはExample Based Testingと呼ばれるもので、いわゆるUnitTestが指しているもののはず。
名前の通り、あるテストデータを用意してそれに対して条件を満たすかどうか判定するテストのこと。

Scalaのような静的型付け言語はコンパイルが通れば動くことはおおよそ保証出来るため、
UnitTestの対象としてはドメインロジックの実装が正しいかどうかのテストになることが多いはず。

関数やメソッドに入力を与えて得られる出力が妥当であるかどうかを判断するケースを書くことになる。
でもテストのための入力、つまりテストデータを用意するのって面倒くさいですよね...？

そこでProperty Based Testingを使ってみるとちょっと楽になるかも。

## Property Based Testingって？

テストデータをランダムに生成して、生成された値に対して条件を満たすかどうか判定するテスト。
簡単にいうとテストデータを生成するためのルールだけ記述すれば、具体的なテストデータについては考えなくて良くなる。

### どんな感じ？

Scala + [rickynils/scalacheck](https://github.com/rickynils/scalacheck/) + ScalaTestで書くとこんな雰囲気。

```scala
forAll(Gen.alphaStr) { alphaStr: String =>
  alphaStr shouldBe alphaStr.reverse.reverse
}
```

英字からなる文字列を自動生成したものと、それをreverseしてreverseしたものが全て等しくなることをテストしている。
`forAll`に対して`Gen[A]`を渡すと`A`が自動生成され、その`A`を使ったブロック内のテストを実行・評価する。

`Gen[A]`だけでなく`Arbitrary[A]`というのもあって、それならimplicitで渡すことが出来る。

```scala
implicit val arbStr = Arbitrary { Gen.alphaStr }
forAll { alphaStr: String =>
  alphaStr shouldBe alphaStr.reverse.reverse
}
```

`forAll`に明示的に`Gen`を渡すか`Arbitrary`をimplicitに渡すかで特に挙動は変わらないので、お好みでいいはず。

## Property Based Testingは何が嬉しいのか

私見ですが、ざっとこんな感じ。

- データの生成を自動化出来る
  - 生成のルールさえ記述すれば良い
  - テストデータが1つ1つ正しいかどうかチェックしなくてよい
  - 生成のルールさえ確認すれば良い
    - コードレビューも楽になる
- 取りうる値に対する認識が深まる
  - 生成ルールを作るためには境界条件を知っていないといけない
  - 自動生成したデータに勝手に境界値が入ってくれる
  - 結果としてドメインに対する理解も深まる

## 書くための準備

ScalaのPropertyBasedTestingフレームワークとして今回は[rickynils/scalacheck](https://github.com/rickynils/scalacheck/)を使う。

### 依存の追加

UTの一部としてPropertyBasedTestを記述できるように、ScalaTestと併用する。
詳しくは[ScalaTestのドキュメント](http://www.scalatest.org/user_guide/property_based_testing)を参照。

build.sbtに以下を書く。

```scala
libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "3.0.4" % Test,
  "org.scalacheck" %% "scalacheck" % "1.13.4" % Test
)
```

これで他のテスト(XxxSpec)を書きつつ、必要に応じてPropertyBasedTestを入れていける。

### 基底クラスの実装

ScalaTestとScalaCheckを併用するための準備としてUTを書くクラスの継承元にあたるクラスを定義しておく。
今回はScalaCheckの`Gen`を使ってデータ生成したいので、`GeneratorDrivenPropertyChecks`をextendsする。
大体こんな感じになるはず。

```scala
import org.scalacheck.Gen
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

class MyPropertyBasedSpec extends FeatureSpec 
  with GeneratorDrivenPropertyChecks with Matchers {
  feature("feature") {
    scenario("scenario") {
      forAll(Gen.alphaStr) { alphaStr: String =>
        alphaStr shouldBe alphaStr.reverse.reverse
      }
    }
  }
}
```

`sbt test`で普通にテストを実行すればSuccessする。

### どんな感じ? 2nd

もうちょっとそれっぽい例。

```scala
object Age {
  def create(value: Int): Option[Age] =
    if (value >= 0 && value <= 120) { // バリデーション
      Some(new Age(value))
    } else None
  }
}
```

この`Age.create`をテストしたい。
つまり、`Age.create`は入力が0~120の値なら`Some`でそうでないなら`None`を返すことをテストしたい。

PropertyBasedTestingなスタイルで書いてみるとこんな感じ。

```scala
feature("Age with Property") {
  scenario("create valid age") { // 成功ケース
    forAll(Gen.chooseNum(0, 120)) { value: Int =>
      Age.create(value).isDefined shouldBe true
    }
  }

  scenario("create invalid age") { // 失敗ケース
    val gen = Gen.oneOf(
      Gen.negNum[Int],   // 負数
      Gen.posNum[Int].suchThat { _ > 120 } // 正数かつ120より大きい
    )
    forAll(gen) { value: Int =>
      Age.create(value).isDefined shouldBe false
    }
  }
}
```

何をしているかは何となく察せるはず。 
今回だとバリデーションを通る/通らない"具体的な"値は用意せずに、ルールだけを記述してある。

ここで[`Gen#suchThat`](https://github.com/rickynils/scalacheck/blob/bc672d9b35aa85a430cd4a8e31991dd3064b73ee/src/main/scala/org/scalacheck/Gen.scala#L111)を使っているが、特定の条件を満たす値を生成したい場合にはこれを使うと良い。
あるいは、[`Whenever#whenever`](http://doc.scalatest.org/1.8/org/scalatest/prop/Whenever.html)を使うと良い。
こちらは生成された値をfilterして、特定の条件を満たすものだけでテストを実施する。

## いろんなものを生成してみる

ベースになる`Gen`の実装は[scalacheck/Gen.scala](https://github.com/rickynils/scalacheck/blob/bc672d9b35aa85a430cd4a8e31991dd3064b73ee/src/main/scala/org/scalacheck/Gen.scala#L208)を参照。


### 文字列の自動生成

まずはシンプルに文字列を生成して組み合わせてみる

```scala
scenario("Gen.alphaStr") {
  // 英字と数字を生成
  forAll(Gen.alphaLowerStr, Gen.numStr) { (alpha, num) =>
    whenever(alpha.nonEmpty && num.nonEmpty) { // filter的な処理
      (alpha + num).matches("^[a-z]+[0-9]+$") shouldBe true
    }
  }
}
```

### `java.util.Calendar`の自動生成

こんなのもある

```scala
scenario("Gen.calendar") {
  // java.util.Calendarを生死絵
  forAll(Gen.calendar) { calendar: Calendar =>
    whenever(calendar.get(Calendar.ERA) == GregorianCalendar.AD) {
      // なんか適当なテスト
      val before = calendar.get(Calendar.YEAR)
      calendar.add(Calendar.YEAR, 100)
      val after = calendar.get(Calendar.YEAR)
      (before + 100) shouldBe after
    }
  }
}
```

### 電話番号を自動生成

`Gen.listOfN`と`Gen.numChar`のあわせ技でやれる

```scala
scenario("Telephone") {
  // for式で合成できる
  val telGen = for {
    z <- Gen.const(0)
    n1 <- Gen.listOfN(2, Gen.numChar)
    n2 <- Gen.listOfN(4, Gen.numChar)
    n3 <- Gen.listOfN(4, Gen.numChar)
  } yield { s"$z${n1.mkString}-${n2.mkString}-${n3.mkString}" }

  forAll(telGen) { tel =>
    println(s"tel: $tel")
    tel.matches("\\d+{3}-\\d+{4}-\\d+{4}") shouldBe true
  }
}
```

こんな感じで生成される

```
tel: 068-3959-7993
tel: 032-3444-9758
tel: 088-2730-2448
tel: 026-6474-0285
tel: 054-0625-8596
tel: 081-8973-9863
```

### 独自クラスの自動生成

↑の電話番号に対応するクラスを生成してみる

```scala
case class TelephoneNumber(value: String)
implicit val telArbitrary = Arbitrary {
  for {
    z <- Gen.const(0)
    n1 <- Gen.listOfN(2, Gen.numChar)
    n2 <- Gen.listOfN(4, Gen.numChar)
    n3 <- Gen.listOfN(4, Gen.numChar)
  } yield { TelephoneNumber(s"$z${n1.mkString}-${n2.mkString}-${n3.mkString}") }
}
```

`Arbitrary[TelephoneNumber]`にしてみたので、`forAll`に渡さなくても型を指定すれば勝手にやってくれる。

```scala
forAll { tel: TelephoneNumber =>
  println(s"tel: $tel")
  tel.value.matches("\\d+{3}-\\d+{4}-\\d+{4}") shouldBe true
}
```

### 決まった文字数の文字列を生成する

パスワードって8文字-30文字だったりする。

```scala
feature("string generator") {
  scenario("between 8 and 30") {
    val strGen: Gen[String] = for {
      n <- Gen.chooseNum(8, 30) // 8-30で文字数を適当に選択
      cs <- Gen.listOfN(n, Gen.alphaNumChar) // その長さのList[Char]を生成
    } yield {
      cs.mkString
    }
  
    forAll(strGen) { (s: String) =>
      s.length >= 8 shouldBe true
      s.length <= 30 shouldBe true
    }
  }
}
```

#### それっぽい例

さっきの生成器を使ってパスワードのテストしてみる。
scalazの`Validation`を使って

```scala
import scalaz._; import Scalaz._

object Password {
  private val spec: String => ValidationNel[String, Unit] = { s: String =>
    if (s.length >= 8 && s.length <= 30) ().success
    else "password length must be between 8 and 30".failureNel
  }

  def create(raw: String): ValidationNel[String, Password] = {
    spec.apply(raw) match {
      case Success(b) => Password(raw).success
      case Failure(msgs) => msgs.failure
    }
  }
}
```

#### テストを書く

まずは前述の長さ指定で文字列を生成する`Gen[String]`を作る関数を実装する。

```scala
def strGenWithMinMax(min: Int, max: Int): Gen[String] = for {
  n <- Gen.chooseNum(min, max) // min~maxな長さを指定
  cs <- Gen.listOfN(n, Gen.alphaNumChar) // その長さのList[Char]を生成
} yield {
  cs.mkString
}
```

これを使ったテストはだいたいこんな感じ。

```scala
scenario("success with proper string") {
  val properStrGen = strGenWithMinMax(8, 30) // 8-30文字
  forAll(properStrGen) { (s: String) =>
    Password.create(s).isSuccess shouldBe true
  }
}
scenario("fail with non-proper string") {
  val nonProperStrGen = Gen.oneOf(
    strGenWithMinMax(0, 7), // 0-7文字
    strGenWithMinMax(31, 100) // 31~文字
  )
  forAll(nonProperStrGen) { (s: String) =>
    Password.create(s).isFailure shouldBe true
  }
}
```

## テストがGave Upする問題

例えば以下のようなテストは、本来パスするはずがこけてしまう。

```scala
scenario("discarded") {
  forAll { s: String =>
    whenever(s.length > 10000) {
      s.length > 10000 shouldBe true
    }
  }
}
```

実行するとこける。

> [info]   Scenario: discarded *** FAILED ***
> [info]   Gave up after 0 successful property evaluations. 51 evaluations were discarded.

なぜかというと、生成した値に対してテストが何も実行されず捨てられてしまっているため、
結果として生成したデータに対してテストがパスしていないことになっている。
ちなみにこれは`suchThat`でも`whenver`でもどちらでも発生する。

### テスト実行の設定

本当は前述した`Gen.listOfN`などを用いて実装するほうが良いが、テスト実行の設定を変更することでも回避できる。
[ドキュメント](http://www.scalatest.org/user_guide/generator_driven_property_checks)によると以下のように書けばテスト実行時の設定を変更できる。

```scala
implicit val generatorDrivenConfig = PropertyCheckConfig(minSize = 10, maxSize = 20)
```

詳しい項目は[ドキュメント](http://www.scalatest.org/user_guide/generator_driven_property_checks)および[実装](https://github.com/scalatest/scalatest/blob/f15205d5c372611f21a9484adf323637c48a79b0/scalatest/src/main/scala/org/scalatest/prop/Configuration.scala#L44)を参照。

[scalatest/Generator.scala](https://github.com/scalatest/scalatest/blob/d40d278f2bb8e73431b942d87881fb6dbab75cb9/scalatest/src/main/scala/org/scalatest/prop/Generator.scala)のあたりの実装を見ると、指定した`minSize`と`sizeRange`から生成する文字列長を決定している様子。

が、思うように適用されておらずここはいまいちわかっていない...。

```scala
scenario("discarded") {
  // `generatorDrivenConfig`という名前で定義する
  implicit val generatorDrivenConfig = PropertyCheckConfiguration(
    minSize = PosZInt(10001)
  )
  forAll { s: String =>
    whenever(s.length > 10000) {
      s.length > 10001 shouldBe true
    }
  }
}
```

これだと10001以上のlengthのStringが生成されてテストはdiscardされないはずなのだが、同様のエラーでこけてしまう。
`minSize = PosZInt(100001)`くらいにするとパスする...。
ともかく、実行に関して設定出来るのでおぼえておくとよい。

たとえば`minSuccessful`はデフォルトで10なので10回成功したらパスしたと見なすが、もう少し厳重にチェックしたいのであれば数を大きくすれば良い。

## まとめ

Property Based Testing、単にテストデータの自動生成する程度にカジュアルに使えるので、テストコード書くのを楽にしていってみましょう。