如何在Haskell中使用SmallCheck?

时间:2013-05-15 01:21:14

标签: testing haskell automated-tests monads smallcheck

我正在尝试使用SmallCheck来测试Haskell程序,但我无法理解如何使用该库来测试我自己的数据类型。显然,我需要使用Test.SmallCheck.Series。但是,我发现它的文档非常令人困惑。我对菜谱式解决方案和逻辑(monadic?)结构的可理解解释感兴趣。以下是我的一些问题(全部相关):

  • 如果我的数据类型为data Person = SnowWhite | Dwarf Integer,我如何向smallCheck解释有效值为Dwarf 1Dwarf 7(或SnowWhite })?如果我有一个复杂的FairyTale数据结构和一个构造函数makeTale :: [Person] -> FairyTale,我希望smallCheck使用构造函数从Person-s列表中创建FairyTale,该怎么办?

    我设法使quickCheck像这样工作,而不会因为Control.Monad.liftMmakeTale这样的功能使用smallCheck的明智应用而弄得太脏。我无法通过Serial找到解决方法(请向我解释一下!)。

  • SeriescoSeries等类型之间的关系是什么?

  • (可选)Positive有什么意义?如何使用SmallCheck.Series中的smallCheck类型?

  • (可选)在smallCheck的上下文中,任何阐明应该是monadic表达式的逻辑是什么,以及什么只是常规函数,将不胜感激。

如果有任何使用smallCheck的介绍/教程,我会很感激。非常感谢你!

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

更新2 :我将有关奇怪的Test.SmallCheck.list的问题移到了{{1}}以及其他地方的separate question

3 个答案:

答案 0 :(得分:17)

注意:此答案描述了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方法,我将在下面简要介绍。


那么我们怎样才能制作SeriesPerson个?这部分很简单

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 NNSerialSerialResults个实例到Serial Person的实例。因此我们使用它来创建一个函数,从[0..7],一个先前定义的Serial值,到我们需要的任何值,然后我们将Person映射到数字并传入'​​em。 / p>


现在我们有Serial的{​​{1}}实例,我们可以使用它来构建更复杂的嵌套FairyTale实例。对于“实例”,如果PersonSerial a => Serial [a]的列表,我们可以使用Serial Person实例与Serial FairyTale实例一起轻松创建instance Serial FairyTale where series = map makeFairyTale . series coseries results = map (makeFairyTale .) . coseries results

(makeFairyTale .)

makeFairyTale组合coseries生成每个函数{{1}},这有点令人困惑)

答案 1 :(得分:4)

  • 如果我的数据类型为data Person = SnowWhite | Dwarf Integer,如何向smallCheck解释有效值为Dwarf 1Dwarf 7(或SnowWhite)?

首先,您需要确定要为每个深度生成哪些值。这里没有单一的正确答案,这取决于您希望搜索空间的细化程度。

以下是两种可能的选择:

  1. people d = SnowWhite : map Dwarf [1..7](不依赖于深度)
  2. people d = take d $ SnowWhite : map Dwarf [1..7](每个深度单位将搜索空间增加一个元素)
  3. 在您决定之后,您的Serial实例就像

    一样简单
    instance Serial m Person where
        series = generate people
    

    我们在这里留下了m多态,因为我们不需要底层monad的任何特定结构。

    • 如果我有一个复杂的FairyTale数据结构和一个构造函数makeTale :: [Person] -> FairyTale怎么办?我希望smallCheck使用构造函数从Person-s列表中创建FairyTale?

    使用cons1

    instance Serial m FairyTale where
      series = cons1 makeTale
    
    • 类型SerialSeries等之间的关系是什么?

    Serial是一个类型类; Series是一种类型。您可以拥有相同类型的多个Series - 它们对应于枚举该类型值的不同方法。但是,为每个值指定应如何生成它可能是艰巨的。 Serial类允许我们为生成特定类型的值指定一个很好的默认值。

    Serial的定义是

    class Monad m => Serial m a where
      series   :: Series m a
    

    所以它只是将特定Series m a分配给ma的给定组合。

    • coseries有什么意义?

    需要生成功能类型的值。

    • 如何使用Positive中的SmallCheck.Series类型?

    例如,像这样:

    > smallCheck 10 $ \n -> n^3 >= (n :: Integer)
    Failed test no. 5.
    there exists -2 such that
      condition is false
    
    > smallCheck 10 $ \(Positive n) -> n^3 >= (n :: Integer)
    Completed 10 tests without failure.
    
    • 在smallCheck的上下文中,任何关于应该是monadic表达式的逻辑是什么以及什么只是常规函数的任何说明都将不胜感激。

    当您编写Serial实例(或任何Series表达式)时,您在Series m monad中工作。

    在编写测试时,您将使用返回BoolProperty m的简单函数。

答案 2 :(得分:1)

虽然我认为@ tel的答案是一个很好的解释(我希望smallCheck实际按照他描述的方式工作),但他提供的代码对我不起作用(使用smallCheck版本1) 。我设法让以下工作......

  

更新/警告下面的代码错误有一个相当微妙的原因。有关更正后的版本和详细信息,请参阅this answer下面提到的问题。简短版本是代替instance Serial Identity Person必须写instance (Monad m) => Series m Person

...但我发现使用Control.Monad.Identity并且所有编译器标志都很奇怪,我已经问过separate question

另请注意,虽然Series Person(或实际上Series Identity Person)实际上与函数Depth -> [Person]不完全相同(请参阅@ tel的答案),但函数generate :: Depth -> [a] -> Series m a之间转换它们。

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, UndecidableInstances #-}
import Test.SmallCheck
import Test.SmallCheck.Series
import Control.Monad.Identity

data Person = SnowWhite | Dwarf Int

instance Serial Identity Person where
        series = generate (\d -> SnowWhite : take (d-1) (map Dwarf [1..7]))