基于性质的测试

在Scala社区,Scala是基于性质测试库的实现之一。在本章我们将实现一个自己的测试库,我们需要学习设计一个库应该做出哪些取舍,又应该总结其中的一些设计思路。
首先在设计API的时候我们应该选择正确的数据类型和函数。那么数据类型和函数应该长什么样子呢?如果没有思路的话,我们可以从ScalaCheck的示例中去发掘,也许会有不错的启示。来看下代码:

val intList = Gen.listOf(Gen.choose(0, 100))
val prop = 
     forAll(intList)(ns => ns.reverse.reverse == ns) && 
     forAll(intList)(ns => ns.headOption == nc.reverse.lastOption)

尽管我们还不能知道listOf和choose的实现,但是我们还是可以通过上面的代码推导出这两个函数的签名,choose应该是(Int, Int) => Gen[Int], listOf应该是Gen[Int] => Gen[List[Int]]。这里对Int应该需要泛化一些,来看下代码:

def listOf[A](ga: Gen[A]): Gen[List[A]] = ???

上面的listOf并没有指定生产List[A]的长度,显然我们应该对listOf进行下重构,代码如下:

def listOfN[A](n: Int, ga: Gen[A]): Gen[List[A]] = ???

现在我们需要思考该怎么实现上面的API函数了,不过先来看下forAll的函数签名,因为他也是一个非常重要的函数。从上面的例子可以看出forAll接受一个Gen[List[Int]]和一个predicate:List[Int] => Boolean。当然想要得到forAll的函数签名还是需要泛化一下的,来看下代码:

  def forAll[A](ga: Gen[A])(f: A => Boolean): Prop = ???

forAll的返回值是Prop,在目前为止我们还不太清楚Prop的详情,但是至少我们可以推断出,他应该是下面这样的结构:

trait Prop {

  def &&(p: Prop): Prop
}

API的函数前面我们已经列出来了, 现在需要归纳下他们的含义:

  • forAll 用来创建性质
  • && 用来组合性质
  • check 用来组合性质
    那么check方法该如何实现呢?我们来做一个练习:
trait Prop {

  def check: Boolean
  
  //假如check的定义如上,该如何实现&&
  def &&(p: Prop): Prop = {
    val c = check
    new Prop {
      override def check: Boolean = c && p.check
    }
  }
}

在上面的实现中Prop和Booean是很想的,但是Boolean并不能表示Prop的全部。一旦性质失败我们需要知道导致失败的性质是哪些,失败前有哪些性质是测试成功的,如果成功我们也需要知道运行了多少个性质。所以我们得重新定义check函数的返回类型。我们既需要知道性质失败的原因,也需要知道成功性质的个数,来看下代码:

package org.fp.scala.check

import org.fp.scala.check.Prop._

trait Prop {

  def check: Either[(FailedCase, SuccessCount), SuccessCount]
  
  def &&(p: Prop): Prop = ???

}

object Prop {

  type FailedCase = String
  type SuccessCount = Int
}

现在回头来看下生成器Gen的API和意义,Gen[A]负责生成A类型的值。在这里可以是随机生成,想一想之前的章节我们实现了一个纯函数的数字生成器,利用这个生成器我们可以将Gen设计成一个包装类型,其中封装了随机数生成器的状态机。

case class Gen[A](simple: State[RNG, A])

知道了Gen的定义我们来看一些练习:

  //利用Gen的定义实现choose函数,生成指定范围内的整数(不含有有边界)
  def choose(start: Int, stopExclusive: Int): Gen[Int] = {
    import RNG._
    Gen(State(map(int)(i => i % stopExclusive + start)))
  }

  //基于这样的定义,请尝试实现unit、boolean和listOfN
  def unit[A](a: => A): Gen[A] =
    Gen(State.unit(a))

  def boolean: Gen[Boolean] =
    unit(true)

  def listOfN[A](n: Int, ga: Gen[A]): Gen[List[A]] = {
    val list = List.fill(n)(ga.simple)
    Gen(State.sequence(list))
  }

  def option[A](ga: Gen[A]): Gen[Option[A]] = {
    Gen(ga.simple.map(Option.apply))
  }

假如我们有一个Gen[Int]可以生成一个0-11之间的整数,利用这个整数我们再来生成一个长度为N的Gen[List[Int]]。这其中有一个规律就是我们得先生成一个值,然后在利用这个值再生成下一个值。这个时候我们就需要flatMap了,来看一组练习:

case class Gen[A](simple: State[RNG, A]) {

  def flatMap[B](f: A => Gen[B]): Gen[B] =
    Gen(simple.flatMap(a => f(a).simple))

  def listOfN(size: Gen[Int]): Gen[List[Int]] =
    size.flatMap(i => Gen.listOfN(i, size))
}

  //实现union将两个同类型的生成器合成一个
  def union[A](g1: Gen[A], g2: Gen[A]): Gen[A] =
    g1.flatMap(_ => g2)

  //实现weighted,类似union根据权重来取值
  def weighted[A](g1: (Gen[A], Double), g2: (Gen[A], Double)): Gen[A] = {
    val (ga1, d1) = g1
    val (ga2, d2) = g2
    if (d1 > d2) ga1 else ga2
  }

现在需要回过头来再看下Prop的定义,Prop算是一个不太严谨的Either,虽然有SuccessCount来表示成功测试用例的个数,但是假如想表示确定多少个测试用例通过才能算测试通过,就只能硬编码了。这里我们不能硬编码,需要把他变成参数。代码如下:

  type FailedCase = String
  type SuccessCount = Int
  type TestCase = Int


sealed trait Result {
  def isFailed: Boolean
}

case object Passed extends Result {
  def isFailed: Boolean = false
}

case class Failed(failure: FailedCase, successes: SuccessCount) extends Result {
  def isFailed: Boolean = true
}

想一想上面的信息足够表示Prop吗?我们过头来看下forAll的实现,依靠上面的API能够实现forALL吗?可以看出forAll没有足够的信息返回一个Prop,基于Gen的随机生成器就需要一个RNG,我们可以将这种依赖放在Prop上。

case class Prop(run: (TestCase, RNG) => Result)

现在我们有足够的信息来实现forAll了,下面是代码实现:

  def forAll[A](ga: Gen[A])(f: A => Boolean): Prop =
    Prop { (n, rng) =>
      randStream(ga)(rng).zip(Stream.from(0)).take(n).map {
        case (a, i) => try {
          if (f(a)) Passed else Failed(a.toString, i)
        } catch {
          case e: Exception => Failed(buildMsg(a, e), i)
        }
      }
      .find(_.isFailed)
      .getOrElse(Passed)
    }

  def randStream[A](ga: Gen[A])(rng: RNG): Stream[A] =
    Stream.unfold(rng)(rng => Some(ga.simple.run(rng)))

  def buildMsg[A](a: A, e: Exception): String =
    s"test case: $a\n" +
    s"generated an exception: ${e.getMessage}\n" +
    s"stack trace: \n ${e.getStackTrace.mkString("\n")}"

上面就是forAll的全部实现了,注意我们捕获了异常并标识为测试失败,而不是让异常乱飞。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,134评论 9 118
  • Java其实不是编译型语言,java编译之后不是计算机可识别的二进制文件,而是一种特殊的class文件,这种文件只...
    Dane_404阅读 487评论 0 0
  • Python语言特性 1 Python的函数参数传递 看两个如下例子,分析运行结果: 代码一: a = 1 def...
    时光清浅03阅读 466评论 0 0
  • 黑色的海岛上悬着一轮又大又圆的明月,毫不嫌弃地把温柔的月色照在这寸草不生的小岛上。一个少年白衣白发,悠闲自如地倚坐...
    小水Vivian阅读 3,092评论 1 5
  • 渐变的面目拼图要我怎么拼? 我是疲乏了还是投降了? 不是不允许自己坠落, 我没有滴水不进的保护膜。 就是害怕变得面...
    闷热当乘凉阅读 4,233评论 0 13