Search code examples
haskellquickcheckproperty-testing

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.


Solution

  • 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.