我是QuickCheck
的新手,无法完全理解如何使用它。
假设我意外地使用Set
(而不是List
)实施了数据类型:
data Profile = Profile (Set Strategy)
--for completeness:
data Strategy = Strategy Int
然后遇到这个bug,两个对象相等,即使它们不应该:
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上查看examples,但似乎它们不包括这些琐碎的案例。
欢迎任何提示!
答案 0 :(得分: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]
答案 1 :(得分: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 = (/=)
这就是我对我的属性进行推理的原因:嗯,对于空和单例列表情况,reverse
只是标识,因此我们希望mkProfile xs
与mkProfile (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 >= 2
,xs == 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
实现中压缩了这个错误,而且还更好地理解了我们的代码及其所遵循的属性!
答案 2 :(得分: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