1

我是新手,QuickCheck不能完全理解如何使用它。

假设我不小心用 a Set(而不是 a List)实现了一个数据类型:

data Profile = Profile (Set Strategy)
--for completeness:
data Strategy = Strategy Int

然后后来遇到了这个错误,其中两个对象是相等的,即使它们不应该:

Profile (Set.fromList [1,2,3]) == Profile (Set.fromList [2,1,3])
-- D'OH! Order doesn't matter in sets!

如何编写QuickCheck测试用例来测试这种情况?在伪代码中,这看起来像这样:

assertNotEqual(Profile (Set.fromList [1,2,3]), Profile (Set.fromList [2,1,3]))
assertEqual(Profile (Set.empty), Profile (Set.empty ))

我已经尝试查看项目 github 上的示例,但似乎它们并没有涵盖这些微不足道的案例。

欢迎任何提示!

4

3 回答 3

3

我如何编写一个 QuickCheck 测试用例来测试这个用例?

你不应该!QuickCheck 是一种基于属性的测试工具。在基于属性的测试中,您提供数据结构(或其他)的属性,测试工具将自动生成测试用例,以查看该属性是否适用于生成的测试用例。那么,让我们看看如何给出属性而不是给出具体的测试用例[1,2,3],以及为什么属性是有利的!

所以。我从

import Test.QuickCheck
import qualified Data.Set as Set 
import Data.Set (Set)

data Profile = Profile (Set Int)
  deriving (Eq, Show)

mkProfile :: [Int] -> Profile
mkProfile = Profile . Set.fromList

-- | We will test if the order of the arguments matter.
test_mkProfile :: [Int] -> Bool
test_mkProfile xs = (mkProfile xs `comp` mkProfile (reverse xs))
  where comp | length xs <= 1 = (==)
             | otherwise      = (/=)

这就是我对我的属性的推理:嗯,对于空列表和单例列表的情况,thenreverse只是身份,所以我们期望mkProfile xsmkProfile (reverse xs). 正确的?我的意思是mkProfile得到完全相同的论点。在这种情况下length xs >= 2显然reverse xs不是xs。喜欢reverse [1, 2] /= [2, 1]。我们知道 Profile确实关心订单。

现在让我们试试这个ghci

*Main> quickCheck test_mkProfile 
*** Failed! Falsifiable (after 3 tests and 1 shrink):     
[0,0]

现在请注意,我们的代码中实际上有两个错误。一,首先,Profile应该使用列表而不是集合。第二,我们的属性不对!因为即使length xs >= 2xs == reverse (xs)也可能是真的。让我们尝试修复第一个错误,看看 quickcheck 仍然会指出第二个缺陷。

data Profile2 = Profile2 [Int]
  deriving (Eq, Show)

mkProfile2 :: [Int] -> Profile2
mkProfile2 = Profile2 

-- | We will test if the order of the arguments matter.
test_mkProfile2 :: [Int] -> Bool
test_mkProfile2 xs = (mkProfile2 xs `comp` mkProfile2 (reverse xs))
  where comp | length xs <= 1 = (==)
             | otherwise      = (/=)

请记住,我们的代码现在是正确的,但我们的属性有缺陷!

*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
*** Failed! Falsifiable (after 8 tests):                   
[-8,-8]

是的。你还需要思考!或者您可能会因为您的代码实际上通过了 700 个测试用例而产生一切正常的错误印象!好的,现在让我们也修复我们的财产!

test_mkProfile2_again :: [Int] -> Bool
test_mkProfile2_again xs = (mkProfile2 xs `comp` mkProfile2 ys)
  where ys   = reverse xs
        comp | xs == ys  = (==)
             | otherwise = (/=)

现在让我们看看它可以多次工作!

*Main> import Control.Monad
*Main Control.Monad> forever $ quickCheck test_mkProfile2_again
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
... (a lot of times)

万岁。现在,我们不仅消除了Profile实现中的错误,而且对我们的代码及其所遵循的属性也有了更好的理解!

于 2013-02-17T14:54:27.643 回答
3

您可以使用SmallCheck支持的存在量化来做到这一点:

> depthCheck 5 $ exists $ \xs -> (xs :: [Integer]) /= sort xs
    Depth 5:
      Completed 1 test(s) without failure.
> depthCheck 5 $ exists $ \xs -> fromList (xs :: [Integer]) /= fromList (sort xs)
    Depth 5:
      Failed test no. 1. Test values follow.
      non-existence

另一种选择,使用通用量化(也适用于 QuickCheck):

> smallCheck 5 $ \xs ys -> xs /= ys ==> fromList (xs :: [Integer]) /= fromList ys
Depth 0:
  Completed 1 test(s) without failure.
  But 1 did not meet ==> condition.
Depth 1:
  Completed 4 test(s) without failure.
  But 2 did not meet ==> condition.
Depth 2:
  Failed test no. 26. Test values follow.
  [0]
  [0,0]
于 2013-02-17T08:26:13.580 回答
2

正如我所评论的,我回答这个问题的主要问题是你的类型缺乏结构Profile。如果您定义 Profile、一组操作和不变量,那么进行快速检查测试将变得很容易。

例如,假设您有一个配置文件、一种构建配置文件的方法和一种修改配置文件的方法。属性都将是唯一的

 module Profile (Profile, mkProfile, addItem) where
 import Data.Set

 newtype Profile = Profile { unProfile :: Set Int }
   deriving (Eq, Ord, Show)

 mkProfile :: [Int] -> Profile
 mkProfile = Profile . fromList

 addItem :: Int -> Profile -> Profile
 addItem x = Profile . insert x . unProfile

您可以通过在每次操作之前和之后声明属性来使用快速检查来测试这样的 ADT:

 import Test.QuickCheck
 import Profile as P

 prop_unique_list_unique_profile :: [Int] -> [Int] -> Bool
 prop_unique_list_unique_profile xs ys =
    xs /= ys ==> mkProfile xs /= mkProfile ys

 prop_addItem_nonequal :: Int -> [Int] -> Bool
 prop_addItem_nonequal x xs = P.addItem x xs /= xs
于 2013-02-17T18:46:21.930 回答