【问题标题】:How can I test a higher-order function using QuickCheck?如何使用 QuickCheck 测试高阶函数?
【发布时间】:2012-03-14 06:30:05
【问题描述】:

我有一个要测试的高阶函数,我要测试的属性之一是它对传入的函数的作用。为了便于说明,这里是一个人为的示例:

gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a

这个想法大致是这是一个示例生成器。我将从单个a 开始,创建[a] 的单例列表,然后创建[a] 的新列表,直到谓词告诉我停止。调用可能如下所示:

gen init next stop

在哪里

init :: a
next :: [a] -> [a]
stop :: [a] -> Bool

这是我要测试的属性:

在任何对gen init next stop 的调用中,gen 承诺绝不会将空列表传递给next

我可以使用 QuickCheck 测试此属性,如果可以,如何测试?

【问题讨论】:

  • 这是一个相关属性:“对于任何非空输入,next 将产生非空输出”。您可能有兴趣对此进行测试,或者作为您提到的属性的补充。
  • @JohnL 确实如此!但这是next的属性,不是gen,而且next是一阶的,所以我知道如何测试它。

标签: haskell quickcheck


【解决方案1】:

如果您提供gen 的实现会有所帮助,但我是 猜测是这样的:

gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen init next stop = loop [init]
  where
    loop xs | stop xs   = head xs
            | otherwise = loop (next xs)

您要测试的属性是 next 从未提供过 空列表。测试这个的一个障碍是你想检查一个 gen 内部的内部循环不变,因此需要从 外。让我们修改gen 以返回此信息:

genWitness :: a -> ([a] -> [a]) -> ([a] -> Bool) -> (a,[[a]])
genWitness init next stop = loop [init]
  where
    loop xs | stop xs   = (head xs,[xs])
            | otherwise = second (xs:) (loop (next xs))

我们使用second 来自 Control.Arrow。 原来的gen很容易用genWitness:定义

gen' :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen' init next stop = fst (genWitness init next stop)

由于惰性评估,这不会给我们带来太多开销。回到 财产!要启用从 QuickCheck 显示生成的函数, 我们使用模块 Test.QuickCheck.Function。 虽然这里不是绝对必要的,但一个好习惯是 单态属性:我们使用Ints 列表而不是允许 使它们成为单元列表的单态性限制。现在让我们声明 属性:

prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen init (Fun _ next) (Fun _ stop) =
    let trace = snd (genWitness init next stop)
    in  all (not . null) trace

让我们尝试使用 QuickCheck 运行它:

ghci> quickCheck prop_gen

某些东西似乎在循环...当然是的:gen 循环如果 stop 在 来自next 的列表永远不会是True!让我们尝试查看输入跟踪的有限前缀 而是:

prop_gen_prefix :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Int -> Bool
prop_gen_prefix init (Fun _ next) (Fun _ stop) prefix_length =
    let trace = snd (genWitness init next stop)
    in  all (not . null) (take prefix_length trace)

我们现在很快得到一个反例:

385
{_->[]}
{_->False}
2

第二个函数是参数next,如果返回的是空列表, 那么gen 中的循环将给next 一个空列表。

我希望这能回答这个问题,并为您提供一些见解 了解如何使用 QuickCheck 测试高阶函数。

【讨论】:

  • 如果您将gen 更改为gen :: Monad m => a -> ([a] -> m [a]) -> ([a] -> m Bool) -> m a,那么您可以将见证代码放入next(和stop),而不必担心此类日志记录会破坏实现。跨度>
  • 哇。我期待在gen 周围放置某种包装,但我不确定我是否想用这个额外的垃圾来弄脏我的干净gen。我会调查一下并回复你。
  • @NormanRamsey:使用@rampion 的建议,只做monadic,然后使用writer monad 进行跟踪,不需要太多的垃圾。那么next只需要将其输入写入writer monad即可。
  • @NormanRamsey 我认为测试的困难在于 gen 函数毕竟不是那么干净。您是否考虑过将第一个参数设为列表而不是单个元素?这样可以更轻松地测试此属性,因为您可以只传递一个空列表来测试此条件,而不是依赖测试中 next 和 stop 函数的行为。
  • @nightski :这有什么帮助?这将立即侵犯财产。您真正想要展示的属性是内部“循环”,我只是在上面给出了一个示例实现,next 永远不会得到一个空列表,这些列表将来自next 本身以及可能的组合和从循环内部重新排列:这是我们必须测试的。
【解决方案2】:

滥用这一点可能是不好的,但如果 QuickCheck 抛出异常,它会使函数失败。因此,要进行测试,只需给它一个为空案例引发异常的函数。改编danr的回答:

import Test.QuickCheck
import Test.QuickCheck.Function
import Control.DeepSeq

prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen x (Fun _ next) (Fun _ stop) = gen x next' stop `deepseq` True
  where next' [] = undefined
        next' xs = next xs

此技术不需要您修改gen

【讨论】:

  • 这里有一个问题:如果stop 函数永远不会返回True,它将永远循环。用户需要创建一个stop,而不是QuickCheck 提供的函数,它保证在某个时候返回True。缺少像const True这样的功能,这可能很难保证。
猜你喜欢
  • 2019-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多