如果使用生成式测试,我们规定:
  任取一个自然数a,在其上调用identity-nat的结果总是返回a。
  (def test-identity-nat
  (prop/for-all [a (s/gen nat-int?)]
  (= a (identity-nat a))))
  (tc/quick-check 100 test-identity-nat)
  -> {:result false,
  :seed 1477362396044,
  :failing-size 0,
  :num-tests 1,
  :fail [0],
  :shrunk {:total-nodes-visited 0,
  :depth 0,
  :result false,
  :smallest [0]}}
  这个测试尝试对100组生成的自然数(nat-int?)进行测试,但首次运行发现代码发生过变动。失败的数据是0,而且还给出了小失败集[0]。拿着这个小失败集,我们可以快速地重现失败用例,从而修正。
  当然也存在这样的可能:在一次运行中,我们的测试无法发现失败的用例。但是,如果100个测试用例都通过了,至少表明我们程序对于100个随机的自然数都是正确的,和基于用例的测试相比,这如同编织出一道更加紧密的安全网——网孔越小,漏掉的情况也越少。
  Clojure语言之父Rich Hickey推崇Simple Made Easy哲学,受其影响生成式测试在Clojure.spec中有更为简约的表达。以上述为例:
  (s/fdef identity-nat
  :args (s/cat :a nat-int?) ; 输入参数的规格
  :ret nat-int?             ; 返回结果的规格
  :fn #(= (:ret %) (-> % :args :a))) ; 入参和出参之间的约束
  (stest/check `identity-nat)
  fdef宏定义了方法identity-nat的规格,默认情况下会基于参数的规格生成1000组数据进行生成式测试。除了这一好处,它还提供部分类型检查的功能。
  再谈TDD
  TDD(测试驱动开发)是一种驱动代码实现和设计的过程。我们说要先有测试,再去实现;保证实现功能的前提下,重构代码以达到较好的设计。整个过程好比演绎推理,测试是其中的证明步骤,而终实现的功能则是证明的结果。
  对于开发人员而言,基于用例的测试方式是友好的,因为它能简单直接地表达实现的功能并保证其正确性。一旦进入红、绿、重构的节(guai)奏(quan),开发人员根本停不下来,仿佛遁入一种心流状态。只不过问题是,基于用例驱动出来的实现可能并不是恰好通过的。我们常常会发现,在写完上组测试用例的实现之后,无需任何改动,下组测试照常能运行通过。换句话说,实现代码可能做了多余的事情而我们却浑然不知。在这种情况下,我们可以利用生成式测试准备大量符合规格的数据探测程序,以此检查程序的健壮性,让缺陷无处遁形。
  凡是想到的情况都能测试,但是想不到情况也需要测试,这才是生成式测试的价值所在。有人把TDD概念化为“展示你的功能”(Show your work),而把生成式测试归纳为“检查你的功能“(Check your work),我深以为然。
  小结
  回到我们写单元测试的动机上:
  1、驱动和验证功能实现;
  2、保护已有的功能不被破坏。
  基于用例的单元测试和生成式测试在这两点上是相辅相成的。我们可以借助它们尽可能早地发现更多的缺陷,避免它们逃逸到生产环境。