1. 程式人生 > >使用Scala實現一個基於性質的測試庫

使用Scala實現一個基於性質的測試庫

這個例子來源於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

.---.                                                                         
|   |   .              __.....__   .----.     .----.   __.....__              
|   | .'|          .-''         '.  \    \   /    /.-''         '.            
|   |<  |         /     .-''"'-.  `. '   '. /'   //     .-''"'-.  `. .-,.--.  
|   | | |        /     /________\   \|    |'    //     /________\   \|  .-. | 
|   | | | .'''-. |                  ||    ||    ||                  || |  | | 
|   | | |/.'''. \\    .-------------''.   `'   .'\    .-------------'| |  | | 
|   | |  /    | | \    '-.____...---. \        /  \    '-.____...---.| |  '-  
|   | | |     | |  `.             .'   \      /    `.             .' | |      
'---' | |     | |    `''-...... -'      '----'       `''-...... -'   | |      
      | '.    | '.                                                   |_|      
      '---'   '---'