ScalaCheckでケースクラスを生成するジェネレータの作り方

先日社内勉強会でJavaでのプロパティベーステストライブラリとしてjunit-quickcheckが取り上げられていました。

参加者から「単一のプロパティではなく複数のプロパティを持ったクラスのインスタンスを生成したい場合はどうすればよいか?」という質問があり、junit-quickcheckではそのクラス用のジェネレータを書く必要があるということだったのですが、Scala用のプロパティベーステストライブラリであるScalaCheckではどうなっているのか試してみました。

まず基本的な型からです。ScalaCheckでも基本的な型のジェネレータは標準で提供されているので以下のような感じでテストを書くことができます。

import org.scalacheck.Properties
import org.scalacheck.Prop.forAll

object IntSpecification extends Properties("Int") {
  property("add") = forAll { (a: Int, b: Int) =>
    a + b == b + a
  }
}

独自のジェネレータも簡単に書くことができます。

val smallInt = Gen.choose(0, 100)

property("add") = forAll(smallInt, smallInt) { (a: Int, b: Int) =>
  a + b >= a
}

さて、ここからが本題なのですが、ScalaCheckではジェネレータを組み合わせてケースクラスを生成するジェネレータを定義することができます。ジェネレータが書きやすかったり組み合わせがしやすかったりするのはScalaCheckの嬉しいところです。

val userGen = for {
  name <- arbitrary[String]
  age  <- Gen.choose(0, 100)
} yield User(name, age)

property("add") = forAll(userGen, userGen) { (a: User, b: User) =>
  a.age + b.age >= a.age
}

scalacheck-shapelessを使うとケースクラス用のジェネレータをマクロで生成することができます。

github.com

implicitly[Arbitrary[User]]

property("add") = forAll { (a: User, b: User) =>
  a.age + b.age == b.age + a.age
}

ただ、実際にプロパティベーステストを導入する場合要件に応じたカスタムジェネレータを作ることになると思うのであまり使う機会はないかも…。きちんとドメインモデルを設計してプロパティの型を定義している場合は便利かもしれません。