简体   繁体   中英

How do you write a new modifier in QuickCheck

I have come across a few instances in my testing with QuickCheck when it would have simplified things to write my own modifiers in some cases, but I'm not exactly sure how one would do this. In particular, it would be helpful to know how to write a modifier for generators of lists and of numerics (such as Int ). I'm aware of NonEmptyList , and Positive and NonNegative , that are already in the library, but in some instances it would have made my tests clearer if I could have specified something like a list that is not only NonEmpty, but also NonSingleton (so, it has at least 2 elements), or an Int that is greater than 1, not just NonZero or Positive , or an Int(egral) that is even/odd, etc.

There's plenty of way in which you can do that. Here's some examples.

Combinator function

You can write a combinator as a function. Here's one that generates non-singleton lists from any Gen a :

nonSingleton :: Gen a -> Gen [a]
nonSingleton g = do
  x1 <- g
  x2 <- g
  xs <- listOf g
  return $ x1 : x2 : xs

This has the same type as the built-in listOf function, and can be used in the same way:

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

Here I've taken advantage of Gen a being a Monad so that I could write both the function and the property with do notation, but you can also write it using monadic combinators if you so prefer.

The function simply generates two values, x1 and x2 , as well as a list xs of arbitrary size (which can be empty), and creates a list of all three. Since x1 and x2 are guaranteed to be single values, the resulting list will have at least those two values.

Filtering

Sometimes you just want to throw away a small subset of generated values. You can to that with the built-in ==> combinator, here used directly in a property:

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

While this property is tautological, it demonstrates that the predicate you place to the left of ==> ensures that whatever runs on the right-hand side of ==> has passed the predicate.

Existing monadic combinators

Since Gen a is a Monad instance, you can also use existing Monad , Applicative , and Functor combinators. Here's one that turns any number inside of any Functor into an even number:

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

Notice that this works for any Functor f , not just for Gen a . Since, however, Gen a is a Functor , you can still use evenInt :

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

The arbitrary function call here creates an unconstrained Integer value. evenInt then makes it even by multiplying it by two.

Arbitrary newtypes

You can also use newtype to create your own data containers, and then make them Arbitrary instances:

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

This also enables you to implement shrink , if you need it.

You can use the newtype in a property like this:

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

The Arbitrary instance uses arbitrary for the type a to generate an unconstrained value i , then doubles it and adds one, thereby ensuring that the value is odd.

Take a look at the QuickCheck documentation for many more built-in combinators. I particularly find choose , elements , oneof , and suchThat useful for expressing additional constraints.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM