読者です 読者をやめる 読者になる 読者になる

謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

ScalaCheck のジェネレータをムリクリ突っ込んでみた

Scala 勉強 OSS

まず、(ScalaCheck)http://www.scalacheck.org/というのがテストツールの一種。

ブラックボックステスト用のツールで、ランダムに値を生成して関数の挙動を見るというものだ。

sbt 定義は

scalaVersion := "2.10.2"

resolvers ++= Seq(
  "Sonatype OSS" at "https://oss.sonatype.org/content/repositories/releases"
)

libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.11.1"

使い方は公式のサンプルを見ればわかると思う。

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

object StringSpecification extends Properties("String") {

  property("startsWith") = forAll { (a: String, b: String) =>
    (a+b).startsWith(a)
  }

  property("concatenate") = forAll { (a: String, b: String) =>
    (a+b).length > a.length && (a+b).length > b.length
  }

  property("substring") = forAll { (a: String, b: String, c: String) =>
    (a+b+c).substring(a.length, a.length+b.length) == b
  }

}

で、これのテストで使われてる乱数なりランダムなデータってどうやって生成されてるのか?と思って追いかけた。

で、結論から言うと org.scalacheck.Gen[+T] が生成している。つまり、これを実装してしまえばカスタムなジェネレータが設定できるわけだ。

ってことで、カスタムなものを作ってみようと思うのだが、まともに実装するのがつらい。

と思うと、便利な関数を発見した。

  private[scalacheck] def gen[T](f: P => R[T]): Gen[T] = new Gen[T] {
    def doApply(p: P) = f(p)
  }

で、R[T] は object.Gen のパッケージ参照制約付きのインナートレイト。じゃぁやることは決まってて、

package org.scalacheck

case class Dummy(id:Int)

object CustomGenerator {
  private def generateDummy(parameters:Gen.Parameters):Gen.R[Dummy] = new Gen.R[Dummy] {
    protected def result: Option[Dummy] = {
      Some(Dummy(parameters.rng.nextInt()))
    }
  }
  def dummyGen:Gen[Dummy] = Gen.gen(generateDummy)
}

いざ行かんだみーじぇねれーた。

で、さくっとテスる

import org.scalacheck.{CustomGenerator, Properties}
import org.scalacheck.Prop.forAll

object CustomGeneratorSpecification extends Properties("DummyRun") {
  property("sample") = forAll(CustomGenerator.dummyGen) { value =>
    value.id != 0
  }
}

で、実行

[info] Compiling 1 Scala source to D:\sources\ScalaCheck\target\scala-2.10\classes...
[info] Compiling 1 Scala source to D:\sources\ScalaCheck\target\scala-2.10\test-classes...
[info] + DummyRun.sample: OK, passed 100 tests.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 5 s, completed 2013/11/26 22:19:21

ヒャッハー

ついでに forAll のもう一つの定義が

  def forAll[A1,P] (
    f: A1 => P)(implicit
    p: P => Prop,
    a1: Arbitrary[A1], s1: Shrink[A1], pp1: A1 => Pretty
  ): Prop = forAllShrink(arbitrary[A1],shrink[A1])(f andThen p)

なので、Arbitrary を実装すればいいんだな!

まぁでも既に Gen 出来てるなら楽勝

  def dummyGen:Gen[Dummy] = Gen.gen(generateDummy)

  implicit val dummyArbitrary = Arbitrary(dummyGen)

つーことで

import org.scalacheck.{Dummy, CustomGenerator, Properties}
import org.scalacheck.Prop.forAll

object CustomGeneratorSpecification extends Properties("DummyRun") {
  import CustomGenerator._

  def sample(dummy:Dummy) = dummy.id != 0
  property("sample") = forAll { (a1:Dummy) =>
    a1.id != 0
  }
}

ウェーイ