使用Scala實現一個基於性質的測試庫
阿新 • • 發佈:2019-02-05
這個例子來源於scala聖經級教程《Functional Programming in Scala》,由於本人跟著書中的程式碼敲了一遍,然後寫了點測試程式碼驗證了一下正確性,所以就放在這做個備忘吧。貼出來只是為了方便自己,如果看不懂,但是又感興趣的就去看原書吧……
package testing
import laziness.Stream
import state._
import parallelism._
import parallelism.Par.Par
import Gen._
import Prop._
import java.util.concurrent.{ExecutorService, Executors}
import state.RNG.Simple
import language.postfixOps
import language.implicitConversions
case class Prop(run: (MaxSize,TestCases,RNG) => Result) {
def &&(p: Prop) = Prop {
(max,n,rng) => run(max,n,rng) match {
case Passed | Proved => p.run(max, n, rng)
case x => x
}
}
def ||(p: Prop) = Prop {
(max,n,rng) => run(max,n,rng) match {
// In case of failure, run the other prop.
case Falsified(msg, _) => p.tag(msg).run(max,n,rng)
case x => x
}
}
/* This is rather simplistic - in the event of failure, we simply prepend
* the given message on a newline in front of the existing message.
*/
def tag(msg: String) = Prop {
(max,n,rng) => run(max,n,rng) match {
case Falsified(e, c) => Falsified(msg + "\n" + e, c)
case x => x
}
}
}
object Prop {
type SuccessCount = Int
type TestCases = Int
type MaxSize = Int
type FailedCase = String
sealed trait Result {
def isFalsified: Boolean
}
case object Passed extends Result {
def isFalsified = false
}
case class Falsified(failure: FailedCase,
successes: SuccessCount) extends Result {
def isFalsified = true
}
case object Proved extends Result {
def isFalsified = false
}
/* Produce an infinite random stream from a `Gen` and a starting `RNG`. */
def randomStream[A](g: Gen[A])(rng: RNG): Stream[A] =
Stream.unfold(rng)(rng => Some(g.sample.run(rng)))
def forAll[A](as: Gen[A])(f: A => Boolean): Prop = Prop {
(n,rng) => randomStream(as)(rng).zip(Stream.from(0)).take(n).map {
case (a, i) => try {
if (f(a)) Passed else Falsified(a.toString, i)
} catch { case e: Exception => Falsified(buildMsg(a, e), i) }
}.find(_.isFalsified).getOrElse(Passed)
}
// String interpolation syntax. A string starting with `s"` can refer to
// a Scala value `v` as `$v` or `${v}` in the string.
// This will be expanded to `v.toString` by the Scala compiler.
def buildMsg[A](s: A, e: Exception): String =
s"test case: $s\n" +
s"generated an exception: ${e.getMessage}\n" +
s"stack trace:\n ${e.getStackTrace.mkString("\n")}"
def apply(f: (TestCases,RNG) => Result): Prop =
Prop { (_,n,rng) => f(n,rng) }
def forAll[A](g: SGen[A])(f: A => Boolean): Prop =
forAll(g(_))(f)
def forAll[A](g: Int => Gen[A])(f: A => Boolean): Prop = Prop {
(max,n,rng) =>
val casesPerSize = (n - 1) / max + 1
val props: Stream[Prop] =
Stream.from(0).take((n min max) + 1).map(i => forAll(g(i))(f))
val prop: Prop =
props.map(p => Prop { (max, n, rng) =>
p.run(max, casesPerSize, rng)
}).toList.reduce(_ && _)
prop.run(max,n,rng)
}
def run(p: Prop,
maxSize: Int = 100,
testCases: Int = 100,
rng: RNG = RNG.Simple(System.currentTimeMillis)): Unit =
p.run(maxSize, testCases, rng) match {
case Falsified(msg, n) =>
println(s"! Falsified after $n passed tests:\n $msg")
case Passed =>
println(s"+ OK, passed $testCases tests.")
case Proved =>
println(s"+ OK, proved property.")
}
val ES: ExecutorService = Executors.newCachedThreadPool
val p1 = Prop.forAll(Gen.unit(Par.unit(1)))(i =>
Par.map(i)(_ + 1)(ES).get == Par.unit(2)(ES).get)
def check(p: => Boolean): Prop = Prop { (_, _, _) =>
if (p) Passed else Falsified("()", 0)
}
val p2 = check {
val p = Par.map(Par.unit(1))(_ + 1)
val p2 = Par.unit(2)
p(ES).get == p2(ES).get
}
def equal[A](p: Par[A], p2: Par[A]): Par[Boolean] =
Par.map2(p,p2)(_ == _)
val p3 = check {
equal (
Par.map(Par.unit(1))(_ + 1),
Par.unit(2)
) (ES) get
}
val S = weighted(
choose(1,4).map(Executors.newFixedThreadPool) -> .75,
unit(Executors.newCachedThreadPool) -> .25) // `a -> b` is syntax sugar for `(a,b)`
def forAllPar[A](g: Gen[A])(f: A => Par[Boolean]): Prop =
forAll(S.map2(g)((_,_))) { case (s,a) => f(a)(s).get }
def checkPar(p: Par[Boolean]): Prop =
forAllPar(Gen.unit(()))(_ => p)
def forAllPar2[A](g: Gen[A])(f: A => Par[Boolean]): Prop =
forAll(S ** g) { case (s,a) => f(a)(s).get }
def forAllPar3[A](g: Gen[A])(f: A => Par[Boolean]): Prop =
forAll(S ** g) { case s ** a => f(a)(s).get }
val pint = Gen.choose(0,10) map (Par.unit(_))
val p4 =
forAllPar(pint)(n => equal(Par.map(n)(y => y), n))
val forkProp = Prop.forAllPar(pint2)(i => equal(Par.fork(i), i)) tag "fork"
}
case class Gen[+A](sample: State[RNG,A]) {
def map[B](f: A => B): Gen[B] =
Gen(sample.map(f))
def map2[B,C](g: Gen[B])(f: (A,B) => C): Gen[C] =
Gen(sample.map2(g.sample)(f))
def flatMap[B](f: A => Gen[B]): Gen[B] =
Gen(sample.flatMap(a => f(a).sample))
/* A method alias for the function we wrote earlier. */
def listOfN(size: Int): Gen[List[A]] =
Gen.listOfN(size, this)
/* A version of `listOfN` that generates the size to use dynamically. */
def listOfN(size: Gen[Int]): Gen[List[A]] =
size flatMap (n => this.listOfN(n))
def listOf: SGen[List[A]] = Gen.listOf(this)
def listOf1: SGen[List[A]] = Gen.listOf1(this)
def unsized = SGen(_ => this)
def **[B](g: Gen[B]): Gen[(A,B)] =
(this map2 g)((_,_))
}
object Gen {
def unit[A](a: => A): Gen[A] =
Gen(State.unit(a))
val boolean: Gen[Boolean] =
Gen(State(RNG.boolean))
def choose(start: Int, stopExclusive: Int): Gen[Int] =
Gen(State(RNG.nonNegativeInt).map(n => start + n % (stopExclusive-start)))
def listOfN[A](n: Int, g: Gen[A]): Gen[List[A]] =
Gen(State.sequence(List.fill(n)(g.sample)))
val uniform: Gen[Double] = Gen(State(RNG.double))
def choose(i: Double, j: Double): Gen[Double] =
Gen(State(RNG.double).map(d => i + d*(j-i)))
/* Basic idea is to add 1 to the result of `choose` if it is of the wrong
* parity, but we require some special handling to deal with the maximum
* integer in the range.
*/
def even(start: Int, stopExclusive: Int): Gen[Int] =
choose(start, if (stopExclusive%2 == 0) stopExclusive - 1 else stopExclusive).
map (n => if (n%2 != 0) n+1 else n)
def odd(start: Int, stopExclusive: Int): Gen[Int] =
choose(start, if (stopExclusive%2 != 0) stopExclusive - 1 else stopExclusive).
map (n => if (n%2 == 0) n+1 else n)
def sameParity(from: Int, to: Int): Gen[(Int,Int)] = for {
i <- choose(from,to)
j <- if (i%2 == 0) even(from,to) else odd(from,to)
} yield (i,j)
def listOfN_1[A](n: Int, g: Gen[A]): Gen[List[A]] =
List.fill(n)(g).foldRight(unit(List[A]()))((a,b) => a.map2(b)(_ :: _))
def union[A](g1: Gen[A], g2: Gen[A]): Gen[A] =
boolean.flatMap(b => if (b) g1 else g2)
def weighted[A](g1: (Gen[A],Double), g2: (Gen[A],Double)): Gen[A] = {
/* The probability we should pull from `g1`. */
val g1Threshold = g1._2.abs / (g1._2.abs + g2._2.abs)
Gen(State(RNG.double).flatMap(d => if (d < g1Threshold) g1._1.sample else g2._1.sample))
}
def listOf[A](g: Gen[A]): SGen[List[A]] =
SGen(n => g.listOfN(n))
/* Not the most efficient implementation, but it's simple.
* This generates ASCII strings.
*/
def stringN(n: Int): Gen[String] =
listOfN(n, choose(0,127)).map(_.map(_.toChar).mkString)
val string: SGen[String] = SGen(stringN)
implicit def unsized[A](g: Gen[A]): SGen[A] = SGen(_ => g)
val smallInt = Gen.choose(-10,10)
val maxProp = forAll(listOf(smallInt)) { l =>
val max = l.max
!l.exists(_ > max) // No value greater than `max` should exist in `l`
}
def listOf1[A](g: Gen[A]): SGen[List[A]] =
SGen(n => g.listOfN(n max 1))
val maxProp1 = forAll(listOf1(smallInt)) { l =>
val max = l.max
!l.exists(_ > max) // No value greater than `max` should exist in `l`
}
// We specify that every sorted list is either empty, has one element,
// or has no two consecutive elements `(a,b)` such that `a` is greater than `b`.
val sortedProp = forAll(listOf(smallInt)) { l =>
val ls = l.sorted
l.isEmpty || ls.tail.isEmpty || !ls.zip(ls.tail).exists { case (a,b) => a > b }
}
object ** {
def unapply[A,B](p: (A,B)) = Some(p)
}
/* A `Gen[Par[Int]]` generated from a list summation that spawns a new parallel
* computation for each element of the input list summed to produce the final
* result. This is not the most compelling example, but it provides at least some
* variation in structure to use for testing.
*
* Note that this has to be a `lazy val` because of the way Scala initializes objects.
* It depends on the `Prop` companion object being created, which references `pint2`.
*/
lazy val pint2: Gen[Par[Int]] = choose(-100,100).listOfN(choose(0,20)).map(l =>
l.foldLeft(Par.unit(0))((p,i) =>
Par.fork { Par.map2(p, Par.unit(i))(_ + _) }))
def genStringIntFn(g: Gen[Int]): Gen[String => Int] =
g map (i => (s => i))
}
case class SGen[+A](g: Int => Gen[A]) {
def apply(n: Int): Gen[A] = g(n)
def map[B](f: A => B): SGen[B] =
SGen { g(_) map f }
def flatMap[B](f: A => SGen[B]): SGen[B] = {
val g2: Int => Gen[B] = n => {
g(n) flatMap { f(_).g(n) }
}
SGen(g2)
}
def **[B](s2: SGen[B]): SGen[(A,B)] =
SGen(n => apply(n) ** s2(n))
}
object Examples {
def main(args: Array[String]):Unit = {
val rng = Simple(System.currentTimeMillis())
val checkRes1 = Prop check { 100 - 100 + 100 == 100}
println(checkRes1.run(1, 1, rng))
val checkRes2 = Prop check { 100 - 100 + 100 == 0}
println(checkRes2.run(1, 1, rng))
val p2Res = Prop.p2.run(1, 1, rng)
println(p2Res)
val unitRes = Gen.unit(5).sample.run(rng)
println(unitRes)
val productGen = Gen.unit(3) ** Gen.unit(5)
val productRes = productGen.sample.run(rng)
println(productRes)
//建立能夠生成大小在閉區間[1, 100]的隨機數的生成器
val chooseGen = Gen.choose(1, 101)
val size = 5
//建立能夠生成長度為5、元素大小在閉區間[1, 100]的連結串列的生成器
val chooseGenListOfRes = Gen.listOf(chooseGen).g(size).sample.run(rng)
println(chooseGenListOfRes)
// 隨機生成0 ~ 100 之間的數字
val ge1le100Prop = Prop.forAll(chooseGen)((a: Int) => a >= 1 && a <= 100)
// 0 ~ 100之間的數字的平方不可能全部都滿足 <= 2500, 所以後續的測試有很大的概率不會通過
val pow2le2500 = Prop.forAll(chooseGen)((a: Int) => math.pow(a, 2) <= 2500)
val chooseProp = ge1le100Prop && pow2le2500 tag "檢測生成的數字大小在[1, 100]之間出現失敗案例 =>"
val choosePropRes = Prop.run(chooseProp)
val len = 100
//check whether the String-Generator generates string which of the size is equql to len or not
// 測試字串生成器生成的字串的長度是否與指定引數相等
Prop.run(Prop.forAll(Gen.stringN(len)){(a: String) => a.length == len}, 10, 3, rng)
//Prop.run(Prop.forAll(Gen.stringN(len)){(a: String) => a.length != len}, 10, 3, rng)
val li = List(1, 3, 5, 7, 9)
//assert List named li contains odd number only
Prop.run(Prop.forAll(Gen.unit(li)){ li => li.exists(_ % 2 == 1)})
}
}
上述程式碼的執行結果是:
Passed
Falsified((),0)
Passed
(5,Simple(1531276787839))
((3,5),Simple(1531276787839))
(List(30, 80, 26, 18, 18),Simple(236643744399618))
! Falsified after 0 passed tests:
檢測生成的數字大小在[1, 100]之間出現失敗案例 =>
84
+ OK, passed 3 tests.
+ OK, passed 100 tests.
879675643@qq.com lhever
.---.
| | . __.....__ .----. .----. __.....__
| | .'| .-'' '. \ \ / /.-'' '.
| |< | / .-''"'-. `. ' '. /' // .-''"'-. `. .-,.--.
| | | | / /________\ \| |' // /________\ \| .-. |
| | | | .'''-. | || || || || | | |
| | | |/.'''. \\ .-------------''. `' .'\ .-------------'| | | |
| | | / | | \ '-.____...---. \ / \ '-.____...---.| | '-
| | | | | | `. .' \ / `. .' | |
'---' | | | | `''-...... -' '----' `''-...... -' | |
| '. | '. |_|
'---' '---'