16

我正在尝试使用SmallCheck来测试 Haskell 程序,但我无法理解如何使用该库来测试我自己的数据类型。显然,我需要使用Test.SmallCheck.Series。但是,我发现它的文档非常混乱。我对食谱风格的解决方案和逻辑(单子?)结构的可理解解释都感兴趣。这是我的一些问题(所有相关的):

  • 如果我有一个数据类型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,我会很感激一个链接。非常感谢!

更新:我应该补充一点,我找到的最有用和可读的文档smallCheck这篇论文 (PDF)。乍一看,我找不到问题的答案;它更像是一个有说服力的广告,而不是一个教程。

更新 2:我将关于Identity出现在 typeTest.SmallCheck.list和其他地方的怪异的问题移到了一个单独的 question

4

3 回答 3

17

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

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

正如我所暗示的,SmallCheckSerial就像 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?就像coarbitraryArbitraryQuickCheck 的类型类中一样,它让我们构建了一系列“小”函数。请注意,我们在输入类型上编写实例——结果类型在另一个Serial参数中传递给我们(我在下面调用results)。

instance Serial Bool where
  coseries results d = [\cond -> if cond then r1 else r2 | 
                        r1 <- results d
                        r2 <- results d]

这些需要更多的聪明才智来编写,我实际上会推荐你使用alts我将在下面简要描述的方法。


那么我们如何制作一些SeriessPerson呢?这部分很简单

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

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

 coseries results d = [\person -> 
                         case person of
                           SnowWhite -> f 0
                           Dwarf n   -> f n
                       | f <- alts1 results d ]

基本思想是从具有实例的值到 的实例altsN results生成一个Seriesof N-ary 函数。所以我们用它来创建一个从 [0..7]到我们需要的任何值的函数,然后我们将我们的 s 映射到数字并传入它们。NSerialSerialResultsSerialPerson


所以现在我们有了 的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

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

于 2013-05-15T02:58:30.957 回答
4
  • 如果我有一个数据类型data Person = SnowWhite | Dwarf Integer,我该如何解释smallCheck有效值是Dwarf 1通过Dwarf 7(或SnowWhite)?

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

这里只有两个可能的选项:

  1. people d = SnowWhite : map Dwarf [1..7](不取决于深度)
  2. people d = take d $ SnowWhite : map Dwarf [1..7](每个深度单位将搜索空间增加一个元素)

在你决定之后,你的Serial实例就像

instance Serial m Person where
    series = generate people

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

  • 如果我有一个复杂的FairyTale数据结构和一个构造函数makeTale :: [Person] -> FairyTale,并且我想smallCheck使用构造函数从 Person-s 列表中制作 FairyTale-s 怎么办?

使用cons1

instance Serial m FairyTale where
  series = cons1 makeTale
  • Serial类型,Series等之间有什么关系?

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 的上下文中,任何关于什么是单子表达式背后的逻辑以及什么只是常规函数的说明,将不胜感激。

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

在编写测试时,您使用返回Bool或的简单函数Property m

于 2013-07-01T22:55:24.347 回答
1

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

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

...但我发现所有编译器标志的使用Control.Monad.Identity和所有编译器标志都很奇怪,我已经问了一个单独的问题

另请注意,虽然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]))
于 2013-05-15T05:19:43.477 回答