你如何在 QuickCheck 中编写新的修饰符

How do you write a new modifier in QuickCheck

我在使用 QuickCheck 进行测试时遇到过几个实例,在某些情况下它会简化一些事情来编写我自己的修饰符,但我不确定如何做到这一点。特别是,了解如何为列表和数字(例如 Int)的生成器编写修饰符会很有帮助。我知道 NonEmptyListPositiveNonNegative 已经在库中,但在某些情况下,如果我可以指定类似不仅是 NonEmpty,而且是 NonSingleton 的列表(因此,它至少有 2 个元素),或者一个大于 1 的 Int,而不仅仅是 NonZeroPositive,或者一个Int(egral)即even/odd等

有很多方法可以做到这一点。这里有一些例子。

组合器函数

您可以将组合器编写为函数。这是一个从任何 Gen a:

生成 non-singleton 列表的方法
nonSingleton :: Gen a -> Gen [a]
nonSingleton g = do
  x1 <- g
  x2 <- g
  xs <- listOf g
  return $ x1 : x2 : xs

这与built-inlistOf函数的类型相同,使用方法相同:

useNonSingleton :: Gen Bool
useNonSingleton = do
  xs :: [String] <- nonSingleton arbitrary
  return $ length xs > 1

这里我利用了 Gen a 作为 Monad 的优势,这样我就可以用 do 符号来编写函数和 属性,但是你可以如果您愿意,也可以使用 monadic 组合器编写它。

该函数简单地生成两个值,x1x2,以及一个任意大小的列表 xs(可以为空),并创建一个包含所有内容的列表三。由于 x1x2 保证是单个值,因此结果列表将至少包含这两个值。

过滤

有时您只想丢弃生成值的一小部分。您可以使用 built-in ==> 组合器,这里直接在 属性:

中使用
moreThanOne :: (Ord a, Num a) => Positive a -> Property
moreThanOne (Positive i) = i > 1 ==> i > 1

虽然这个 属性 是同义反复,但它表明您放置在 ==> 左侧的谓词可确保在 ==> 的 right-hand 侧运行的任何内容都具有通过了谓词。

现有的单子组合器

由于 Gen a 是一个 Monad 实例,您还可以使用现有的 MonadApplicativeFunctor 组合器。这是将任何 Functor 中的任何数字变成偶数的方法:

evenInt :: (Functor f, Num a) => f a -> f a
evenInt = fmap (* 2)

请注意,这适用于 any Functor f,而不仅仅是 Gen a。但是,由于 Gen aFunctor,您仍然可以使用 evenInt:

allIsEven :: Gen Bool
allIsEven = do
  i :: Integer <- evenInt arbitrary
  return $ even i

此处的 arbitrary 函数调用创建了一个不受约束的 Integer 值。 evenInt 然后乘以 2 使它均匀。

任意新类型

您还可以使用 newtype 创建您自己的数据容器,然后将它们设为 Arbitrary 个实例:

newtype Odd a = Odd a deriving (Eq, Ord, Show, Read)

instance (Arbitrary a, Num a) => Arbitrary (Odd a) where
  arbitrary = do
    i <- arbitrary
    return $ Odd $ i * 2 + 1

如果需要,这也使您能够实施 shrink

您可以像这样在 属性 中使用 newtype

allIsOdd :: Integral a => Odd a -> Bool
allIsOdd (Odd i) = odd i

Arbitrary实例使用arbitrary为类型a生成一个不受约束的值i,然后将其加倍并加一,从而确保该值是奇数

查看 QuickCheck documentation 以获得更多 built-in 组合器。我特别发现 chooseelementsoneofsuchThat 对于表达额外的约束非常有用。