我正在尝试使用SmallCheck测试Haskell程序,但是我无法理解如何使用该库来测试自己的数据类型。显然,我需要使用Test.SmallCheck.Series。但是,我发现它的文档非常混乱。我对食谱风格的解决方案以及对逻辑(monadic?)结构的可理解的解释都感兴趣。这是我有的一些问题(所有相关问题):
data Person = SnowWhite | Dwarf Integer
,如何通过smallCheck
(或Dwarf 1
)向Dwarf 7
解释有效值为SnowWhite
?如果我具有复杂的FairyTale
数据结构和构造函数makeTale :: [Person] -> FairyTale
,并且我希望smallCheck
使用构造函数从Person-s列表中生成FairyTale-s怎么办?通过将明智的
quickCheck
应用程序用于Control.Monad.liftM
之类的功能,我设法使makeTale
像这样工作,而不会弄脏我的双手。我无法找到一种用smallCheck
做到这一点的方法(请向我解释!)。 Serial
,Series
等类型之间的关系是什么? coSeries
的意义是什么?如何使用Positive
中的SmallCheck.Series
类型? 如果有使用
smallCheck
的简介/教程,我希望您能找到一个链接。非常感谢你!更新:我应该补充一点,我发现
smallCheck
最有用,最易读的文档是this paper (PDF)。乍看之下,我找不到问题的答案;它比教程更有说服力。更新2:我将有关
Identity
和其他位置类型的奇怪Test.SmallCheck.list
的问题移到separate question。 最佳答案
注意:该答案描述了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的coarbitrary
类型类中的Arbitrary
一样,它使我们可以构建一系列“小”功能。请注意,我们正在通过输入类型编写实例-结果类型将通过另一个Serial
参数传递给我们(我在下面称为results
)。instance Serial Bool where
coseries results d = [\cond -> if cond then r1 else r2 |
r1 <- results d
r2 <- results d]
这些编写起来需要更多的技巧,实际上我将指导您使用
alts
方法,下面将对其进行简要介绍。那么我们如何制作
Series
的Person
?这部分很容易instance Series Person where
series d = SnowWhite : take (d-1) (map Dwarf [1..7])
...
但是我们的
coseries
函数需要生成从Person
到其他内容的所有可能函数。这可以使用SmallCheck提供的altsN
系列功能来完成。这是一种写法 coseries results d = [\person ->
case person of
SnowWhite -> f 0
Dwarf n -> f n
| f <- alts1 results d ]
基本思想是
altsN results
从具有Series
实例的N
值到N
的Serial
实例生成Serial
-ary函数的Results
。因此,我们使用它来创建一个从[0..7](先前定义的Serial
值)到所需函数的函数,然后将Person
映射到数字并将其传递给他们。因此,现在我们有了
Serial
的Person
实例,我们可以使用它来构建更复杂的嵌套Serial
实例。对于“实例”,如果FairyTale
是Person
的列表,我们可以将Serial a => Serial [a]
实例与Serial Person
实例一起使用以轻松创建Serial FairyTale
:instance Serial FairyTale where
series = map makeFairyTale . series
coseries results = map (makeFairyTale .) . coseries results
(
(makeFairyTale .)
由makeFairyTale
生成的每个函数组成coseries
,这有点令人困惑)