注意:此答案描述了 SmallCheck 的 1.0 之前的版本。有关 SmallCheck 0.6 和 1.0 之间的重要区别,请参阅 this blog post。
SmallCheck 与 QuickCheck 类似,它在可能类型空间的某些部分上测试属性。不同之处在于它试图详尽地枚举一系列所有“小”值,而不是小值的任意子集。
正如我所暗示的,SmallCheck 的 Serial 就像 QuickCheck 的 Arbitrary。
现在Serial 非常简单:Serial 类型 a 有一种方法 (series) 可以生成 Series 类型,它只是来自 Depth -> [a] 的函数。或者,为了解包,Serial 对象是我们知道如何枚举一些“小”值的对象。我们还得到了一个Depth 参数,它控制我们应该生成多少个小值,但我们暂时忽略它。
instance Serial Bool where series _ = [False, True]
instance Serial Char where series _ = "abcdefghijklmnopqrstuvwxyz"
instance Serial a => Serial (Maybe a) where
series d = Nothing : map Just (series d)
在这些情况下,我们只是忽略Depth 参数,然后枚举每种类型的“所有”可能值。我们甚至可以为某些类型自动执行此操作
instance (Enum a, Bounded a) => Serial a where series _ = [minBound .. maxBound]
这是一种非常简单的彻底测试属性的方法——从字面上测试每一个可能的输入!显然,至少存在两个主要缺陷:(1) 无限数据类型在测试时会导致无限循环;(2) 嵌套类型会导致要查看的示例空间呈指数级增长。在这两种情况下,SmallCheck 都会很快变得非常大。
这就是Depth 参数的意义所在——它让系统要求我们将Series 保持小。从文档中,Depth 是
生成测试值的最大深度
对于数据值,是嵌套构造器应用的深度。
对于函数值来说,既是嵌套案例分析的深度,也是结果的深度。
所以让我们重新设计我们的示例以保持它们的小。
instance Serial Bool where
series 0 = []
series 1 = [False]
series _ = [False, True]
instance Serial Char where
series d = take d "abcdefghijklmnopqrstuvwxyz"
instance Serial a => Serial (Maybe a) where
-- we shrink d by one since we're adding Nothing
series d = Nothing : map Just (series (d-1))
instance (Enum a, Bounded a) => Serial a where series d = take d [minBound .. maxBound]
好多了。
那么coseries 是什么?就像 QuickCheck 的 Arbitrary 类型类中的 coarbitrary 一样,它让我们构建了一系列“小”函数。请注意,我们在输入类型上编写实例——结果类型在另一个 Serial 参数中传递给我们(我在下面调用 results)。
instance Serial Bool where
coseries results d = [\cond -> if cond then r1 else r2 |
r1 <- results d
r2 <- results d]
这些需要更多的聪明才智来编写,我实际上会推荐您使用alts 方法,我将在下面简要描述。
那么我们怎样才能制作一些Series 和Persons 呢?这部分很简单
instance Series Person where
series d = SnowWhite : take (d-1) (map Dwarf [1..7])
...
但是我们的coseries 函数需要生成从Persons 到其他东西的所有可能的函数。这可以使用 SmallCheck 提供的altsN 系列函数来完成。这是一种写法
coseries results d = [\person ->
case person of
SnowWhite -> f 0
Dwarf n -> f n
| f <- alts1 results d ]
基本思想是altsN results 生成Series 的N-ary 函数,从具有Serial 实例的N 值到Results 的Serial 实例。因此,我们使用它从 [0..7](先前定义的 Serial 值)到我们需要的任何值创建一个函数,然后我们将 Persons 映射到数字并将它们传入。
现在我们有一个Person 的Serial 实例,我们可以使用它来构建更复杂的嵌套Serial 实例。对于“实例”,如果FairyTale 是Persons 的列表,我们可以在Serial Person 实例旁边使用Serial a => Serial [a] 实例来轻松创建Serial FairyTale:
instance Serial FairyTale where
series = map makeFairyTale . series
coseries results = map (makeFairyTale .) . coseries results
((makeFairyTale .) 与makeFairyTale 组成coseries 生成的每个函数,这有点令人困惑)