跳转到内容

Haskell/测试

来自 Wikibooks,开放世界中的开放书籍

Quickcheck

[编辑 | 编辑源代码]

考虑以下函数

getList = find 5 where
     find 0 = return []
     find n = do
       ch <- getChar
       if ch `elem` ['a'..'e'] then do
             tl <- find (n-1)
             return (ch : tl) else
           find n

我们如何在 Haskell 中有效地测试此函数?我们将使用重构和 QuickCheck。

保持纯净

[编辑 | 编辑源代码]

getList 函数很难测试,因为getChar 在外部世界执行 IO 操作,因此没有内部方法来验证内容。我们do块中的其他语句都与 IO 绑定在一起。

让我们解开我们的函数,以便我们至少可以使用 QuickCheck 测试引用透明的部分。首先,我们可以利用惰性 IO 来避免所有令人不快的底层 IO 处理。

因此,第一步是将函数的 IO 部分分解成一个薄薄的“外壳”层

-- A thin monadic skin layer
getList :: IO [Char]
getList = fmap take5 getContents

-- The actual worker
take5 :: [Char] -> [Char]
take5 = take 5 . filter (`elem` ['a'..'e'])

使用 QuickCheck 进行测试

[编辑 | 编辑源代码]

现在,我们可以独立测试算法的“核心”,即 take5 函数。让我们使用 QuickCheck。首先,我们需要 Char 类型的 Arbitrary 实例——它负责为我们生成随机的 Char 用于测试。出于简单起见,将其限制在一定范围内的良好字符

import Data.Char
import Test.QuickCheck

instance Arbitrary Char where
    arbitrary     = choose ('\32', '\128')
    coarbitrary c = variant (ord c `rem` 4)

让我们启动 GHCi 并尝试一些通用属性(可以直接从 Haskell REPL 使用 QuickCheck 测试框架很不错)。首先是一个简单的,[Char] 等于自身

*A> quickCheck ((\s -> s == s) :: [Char] -> Bool)
OK, passed 100 tests.

刚刚发生了什么?QuickCheck 生成了 100 个随机的 [Char] 值,并应用了我们的属性,检查所有情况下的结果是否为 True。QuickCheck *为我们生成了测试集*!

现在是一个更有趣的属性:反转两次返回恒等式

*A> quickCheck ((\s -> (reverse.reverse) s == s) :: [Char] -> Bool)
OK, passed 100 tests.

太棒了!

测试 take5

[编辑 | 编辑源代码]

使用 QuickCheck 进行测试的第一步是找出函数对所有输入都为真的某些属性。也就是说,我们需要找到*不变式*。

一个简单的不变式可能是:

所以让我们将其写成 QuickCheck 属性

\s -> length (take5 s) == 5

然后我们可以在 QuickCheck 中运行它

*A> quickCheck (\s -> length (take5 s) == 5)
Falsifiable, after 0 tests:
""

啊!QuickCheck 发现了我们的错误。如果输入字符串包含少于 5 个可过滤字符,则结果字符串的长度不会超过 5 个字符。因此,让我们稍微弱化一下属性:

也就是说,take5 返回一个长度最多为 5 个字符的字符串。让我们测试一下

*A> quickCheck (\s -> length (take5 s) <= 5)
OK, passed 100 tests.

好!

另一个属性

[编辑 | 编辑源代码]

另一件事需要检查的是是否返回了正确的字符。也就是说,对于所有返回的字符,这些字符都是集合 ['a','b','c','d','e'] 的成员。

我们可以将其指定为:

在 QuickCheck 中

*A> quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))
OK, passed 100 tests.

优秀。因此,我们可以对该函数既不会返回过长的字符串也不会包含无效字符充满信心。

覆盖率

[编辑 | 编辑源代码]

在测试 [Char] 时,使用默认 QuickCheck 配置的一个问题是:标准的 100 次测试对于我们的情况来说不够。实际上,当使用提供的 Char 的 Arbitrary 实例时,QuickCheck 永远不会生成长度超过 5 个字符的字符串!我们可以确认这一点

*A> quickCheck (\s -> length (take5 s) < 5)
OK, passed 100 tests.

QuickCheck 浪费时间生成不同的 Char,而我们真正需要的是更长的字符串。解决此问题的一种方法是修改 QuickCheck 的默认配置以进行更深入的测试

deepCheck p = check (defaultConfig { configMaxTest = 10000}) p

这指示系统在得出一切正常之前找到至少 10000 个测试用例。让我们检查它是否正在生成更长的字符串

*A> deepCheck (\s -> length (take5 s) < 5)
Falsifiable, after 125 tests:
";:iD^*NNi~Y\\RegMob\DEL@krsx/=dcf7kub|EQi\DELD*"

我们可以使用 'verboseCheck' 钩子检查 QuickCheck 生成的测试数据。这里,在整数列表上进行测试

*A> verboseCheck (\s -> length s < 5)
0: []
1: [0]
2: []
3: []
4: []
5: [1,2,1,1]
6: [2]
7: [-2,4,-4,0,0]
Falsifiable, after 7 tests:
[-2,4,-4,0,0]

关于 QuickCheck 的更多信息

[编辑 | 编辑源代码]

有时为测试提供示例比根据一般规则定义测试更容易。HUnit 提供了一个单元测试框架,可以帮助你做到这一点。你也可以滥用 QuickCheck,通过提供一个恰好适合你的示例的一般规则;但在这种情况下,直接使用 HUnit 可能工作量更少。

待办事项:提供 HUnit 测试示例,并对其进行简要介绍

有关使用 HUnit 的更多详细信息,请参阅其用户指南



华夏公益教科书