Quickcheck:生成任意集合的任意元素

Quickcheck: produce arbitrary elements of an arbitrary set

假设我正在为 Data.Set 编写测试。我想检查从集合中删除元素是否有效,所以我可能会这样写:

prop_deleteA it x = member x it ==> not (member x (delete x it))

假设 it 有合适的 Arbitrary 实例。然而,这依赖于 quickcheck 生成恰好存在于集合中的 x 值,这通常无法保证。如果能使x依赖于it,保证x已经是it的成员就更好了。我该怎么做?

我想到我可以写

prop_deleteB it f = let x = f it
                   in not (member x (delete x it))

其中 f :: Set a -> a 是通过任意方式适当定义的。然而,coarbitrary 只允许我们定义 f :: Set a -> b,不幸的是这不是我们想要的。到目前为止,我最好的想法是定义一个新类型

data SetAndElement a = SetAndElement (Set a) a

这允许我们写一个合适的Arbitrary实例

instance (Ord a, Arbitrary a) => Arbitrary (SetAndElement a) where
    arbitrary = do it <- suchThat arbitrary (not . Set.null)
                   x  <- elements (elems it)
                   return (SetAndElement it x)

允许prop_delete写成

prop_deleteC (SetAndElement it x) = not (member x (delete x it))

这行得通,但似乎有点复杂;有没有更好的选择? (如果不是,我将修改问题并将其作为答案。)实际的 Data.Set 实现(容器包)通过检查 (delete x) . (insert x) == id if x 是否已经不是一个来测试删除给定集合的成员。

这取决于您可用的发电机。例如,如果您已经有 setOf1(生成一个至少包含一个元素的 Set)和 setElements(从 Set 中获取元素),它可以写成 forAll:

-- example implementations of both combinators
setOf1 :: (Arbitrary a, Ord a) => Gen a -> Gen (Set a)
setOf1 = fmap fromList . listOf1

setElements :: Set a -> Gen a
setElements = elements . toList

prop_delete =
  forAll (setOf1 arbitrary)   $ \theSet ->
  forAll (setElements theSet) $ \x ->
    not (member (x :: Int) (delete x theSet))

这与 SetAndElement 基本相同,但我们使用的不是固定的 data 类型,而是可用于进一步测试的可重用函数:

prop_null =  forAll (setOf1 (arbitrary :: Gen Integer)) $ not . null

然而,即使您不写 setOf1setElementsforAll 对于简单测试来说也可能相当简洁:

prop_delete :: (Arbitrary a, Ord a) => (NonEmptyList a) -> Property
prop_delete (NonEmpty xs) =
  let theSet = fromList xs
  in forAll (elements xs) $ \x ->
      not (member x (delete x theSet))

如果你提供setElementsNonEmptySet,这可以写成

newtype NonEmptySet x = NonEmptySet {getNonEmptySet :: Set a}

instance (Ord a, Arbitray a) => Arbitrary (NonEmptySet a) where
  arbitrary = fmap NonEmptySet . setOf1 $ arbitrary

prop_delete :: (Arbitrary a, Ord a) => (NonEmptySet a) -> Property
prop_delete (NonEmptySet theSet) =
  forAll (setElements theSet) $ \x ->
    not (member x (delete x theSet))

这样您就可以将 NonEmptySet 用于需要非空集的测试,而 setElements 仅用于那些您实际需要随机选择元素的测试。