你如何在 QuickCheck 中编写新的修饰符
How do you write a new modifier in QuickCheck
我在使用 QuickCheck 进行测试时遇到过几个实例,在某些情况下它会简化一些事情来编写我自己的修饰符,但我不确定如何做到这一点。特别是,了解如何为列表和数字(例如 Int
)的生成器编写修饰符会很有帮助。我知道 NonEmptyList
、Positive
和 NonNegative
已经在库中,但在某些情况下,如果我可以指定类似不仅是 NonEmpty,而且是 NonSingleton 的列表(因此,它至少有 2 个元素),或者一个大于 1 的 Int
,而不仅仅是 NonZero
或 Positive
,或者一个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 组合器编写它。
该函数简单地生成两个值,x1
和 x2
,以及一个任意大小的列表 xs
(可以为空),并创建一个包含所有内容的列表三。由于 x1
和 x2
保证是单个值,因此结果列表将至少包含这两个值。
过滤
有时您只想丢弃生成值的一小部分。您可以使用 built-in ==>
组合器,这里直接在 属性:
中使用
moreThanOne :: (Ord a, Num a) => Positive a -> Property
moreThanOne (Positive i) = i > 1 ==> i > 1
虽然这个 属性 是同义反复,但它表明您放置在 ==>
左侧的谓词可确保在 ==>
的 right-hand 侧运行的任何内容都具有通过了谓词。
现有的单子组合器
由于 Gen a
是一个 Monad
实例,您还可以使用现有的 Monad
、Applicative
和 Functor
组合器。这是将任何 Functor
中的任何数字变成偶数的方法:
evenInt :: (Functor f, Num a) => f a -> f a
evenInt = fmap (* 2)
请注意,这适用于 any Functor f
,而不仅仅是 Gen a
。但是,由于 Gen a
是 Functor
,您仍然可以使用 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 组合器。我特别发现 choose
、elements
、oneof
和 suchThat
对于表达额外的约束非常有用。
我在使用 QuickCheck 进行测试时遇到过几个实例,在某些情况下它会简化一些事情来编写我自己的修饰符,但我不确定如何做到这一点。特别是,了解如何为列表和数字(例如 Int
)的生成器编写修饰符会很有帮助。我知道 NonEmptyList
、Positive
和 NonNegative
已经在库中,但在某些情况下,如果我可以指定类似不仅是 NonEmpty,而且是 NonSingleton 的列表(因此,它至少有 2 个元素),或者一个大于 1 的 Int
,而不仅仅是 NonZero
或 Positive
,或者一个Int(egral)
即even/odd等
有很多方法可以做到这一点。这里有一些例子。
组合器函数
您可以将组合器编写为函数。这是一个从任何 Gen a
:
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 组合器编写它。
该函数简单地生成两个值,x1
和 x2
,以及一个任意大小的列表 xs
(可以为空),并创建一个包含所有内容的列表三。由于 x1
和 x2
保证是单个值,因此结果列表将至少包含这两个值。
过滤
有时您只想丢弃生成值的一小部分。您可以使用 built-in ==>
组合器,这里直接在 属性:
moreThanOne :: (Ord a, Num a) => Positive a -> Property
moreThanOne (Positive i) = i > 1 ==> i > 1
虽然这个 属性 是同义反复,但它表明您放置在 ==>
左侧的谓词可确保在 ==>
的 right-hand 侧运行的任何内容都具有通过了谓词。
现有的单子组合器
由于 Gen a
是一个 Monad
实例,您还可以使用现有的 Monad
、Applicative
和 Functor
组合器。这是将任何 Functor
中的任何数字变成偶数的方法:
evenInt :: (Functor f, Num a) => f a -> f a
evenInt = fmap (* 2)
请注意,这适用于 any Functor f
,而不仅仅是 Gen a
。但是,由于 Gen a
是 Functor
,您仍然可以使用 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 组合器。我特别发现 choose
、elements
、oneof
和 suchThat
对于表达额外的约束非常有用。