如何在Haskell中使用SmallCheck?

use*_*ame 15 testing monads automated-tests haskell smallcheck

我正在尝试使用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的上下文中,任何阐明应该是monadic表达式的逻辑是什么,以及什么只是常规函数,将会受到赞赏.

如果有任何介绍/教程使用smallCheck,我会很感激一个链接.非常感谢你!

更新:我应该补充一点,我找到的最有用和可读的文档smallCheck本文(PDF).第一眼看我在那里找不到我的问题的答案; 它更像是一个有说服力的广告而不是教程.

更新2:我把关于Identity在类型Test.SmallCheck.list和其他地方出现的怪异问题转移到一个单独的问题上.

J. *_*son 17

注意:此答案描述了SmallCheck的1.0之前的版本.有关SmallCheck 0.6和1.0之间的重要区别,请参阅此博客文章.

SmallCheck与QuickCheck类似,它在可能类型的某些部分空间中测试属性.不同之处在于它试图详尽地枚举一系列所有 "小"值而不是任意小值的子集.

正如我所暗示的,SmallCheck Serial就像QuickCheck 一样Arbitrary.

现在Serial很简单:一个Serial类型a有一个way(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)
Run Code Online (Sandbox Code Playgroud)

在这些情况下,我们只是忽略Depth参数,然后枚举每种类型的"所有"可能值.我们甚至可以为某些类型自动执行此操作

instance (Enum a, Bounded a) => Serial a where series _ = [minBound .. maxBound]
Run Code Online (Sandbox Code Playgroud)

这是一种非常简单的测试属性的方法 - 逐字测试每一个可能的输入!显然,至少存在两个主要缺陷:(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]
Run Code Online (Sandbox Code Playgroud)

好多了.


那是什么coseries?就像coarbitraryArbitraryQuickCheck的类型类中一样,它允许我们构建一系列"小"函数.请注意,我们在输入类型上编写实例 - 结果类型在另一个Serial参数中传递给我们(我在下面调用results).

instance Serial Bool where
  coseries results d = [\cond -> if cond then r1 else r2 | 
                        r1 <- results d
                        r2 <- results d]
Run Code Online (Sandbox Code Playgroud)

这些需要更多的聪明才能写,我实际上会引用你使用alts我将在下面简要描述的方法.


那么怎样才能使一些SeriesPersonS' 这部分很容易

instance Series Person where
  series           d = SnowWhite : take (d-1) (map Dwarf [1..7])
  ...
Run Code Online (Sandbox Code Playgroud)

但是我们的coseries函数需要生成从Persons到其他东西的所有可能函数.这可以使用altsNSmallCheck提供的一系列功能来完成.这是编写它的一种方法

 coseries results d = [\person -> 
                         case person of
                           SnowWhite -> f 0
                           Dwarf n   -> f n
                       | f <- alts1 results d ]
Run Code Online (Sandbox Code Playgroud)

的基本思想是,altsN results产生SeriesN从进制函数N值与Serial实例的Serial实例Results.因此我们使用它来创建一个函数,从[0..7],一个先前定义的Serial值,到我们需要的任何值,然后我们将Persons 映射到数字并传递给它们.


现在我们有了一个Serial实例Person,我们可以用它来构建更复杂的嵌套Serial实例.对于"实例",如果FairyTalePersons 的列表,我们可以使用Serial a => Serial [a]实例与我们的Serial Person实例一起轻松创建Serial FairyTale:

instance Serial FairyTale where
  series = map makeFairyTale . series
  coseries results = map (makeFairyTale .) . coseries results
Run Code Online (Sandbox Code Playgroud)

(与每个函数(makeFairyTale .)组合生成,这有点令人困惑)makeFairyTalecoseries