quickcheck-helpers
What is this?
Consider a typical product type:
data Pair a = Pair a a
To use Pair in QuickCheck tests, you need an Arbitrary instance. For product
types, the arbitrary method is easy to define:
instance Arbitrary a => Arbitrary (Pair a) where
arbitrary = Pair <$> arbitrary <*> arbitrary
However, if we try to define shrink similarly, we will get serious issues:
instance Arbitrary a => Arbitrary (Pair a) where
-- Fine
arbitrary = Pair <$> arbitrary <*> arbitrary
-- Very not fine
shrink (Pair x y) = Pair <$> shrink x <*> shrink y
Why? Consider Pair 0 6 :: Pair Int. We expect that this should still shrink:
even though the first field is not shrinkable, the second one still is. If we
try to shrink, however, we get an unpleasant surprise:
>>> shrink (Pair 0 6 :: Pair Int)
[]
In general, if we define shrinkers for product types in the way shown above,
shrinking will stop when any field is non-shrinkable. This is not a
consequence of the syntax per se: even if we rewrite it monadically, this still
happens.
To avoid this, we have two options. One is to manually write the shrinker in a
very specific way:
instance Arbitrary a => Arbitrary (Pair a) where
-- No change
arbitrary = Pair <$> arbitrary <*> arbitrary
-- Fix, but at what cost?
shrink (Pair x y) = (Pair <$> pure x <*> shrink y) <|>
(Pair <$> shrink x <*> pure y)
This will solve the problem, but it's tedious, error-prone and very long.
Another option is to derive Generic for your type, then use genericShrink
from QuickCheck:
data Pair a = Pair a a
deriving stock (Generic)
instance Arbitrary a => Arbitrary (Pair a) where
-- No change
arbitrary = Pair <$> arbitrary <*> arbitrary
shrink = genericShrink
This will also solve the problem, but comes at significant costs in compile-time
performance, run-time performance and a complete and utter lack of clarity about
what actual code gets generated. These are all problems typical of Generic
use. However, there is also a deeper problem with this solution. Consider the
following data type:
data CountedList a = CountedList Int [a]
deriving stock (Generic)
This data type requires maintaining a non-structural invariant about the Int
field. Specifically, the Int field must be exactly the length of the [a]
field. However, if we attempt to use genericShrink with this type:
instance Arbitrary a => Arbitrary (CountedList a) where
arbitrary = do
NonNegative len <- arbitrary
CountedList len <$> vectorOf len
shrink = genericShrink
you will end up with many nonsensical shrinks:
shrink (CountedList 2 [1, 3] :: CountedList Int)
>>> [CountedList 0 [1,3],CountedList 1 [1,3],CountedList 2 [],CountedList 2 [3],CountedList 2 [1],CountedList 2 [0,3],CountedList 2 [1,0],CountedList 2 [1,2]]
Thus, both solutions are in some way inadequate.
quickcheck-helpers solves this problem, allowing both concise definitions of
shrink for product types (mirroring arbitrary) while also giving full
control if necessary.
How do I use it?
For the Pair example above:
import Test.QuickCheck.Helpers (toShrink, neShrink)
data Pair a = Pair a a
instance Arbitrary a => Arbitrary (Pair a) where
-- No change
arbitrary = Pair <$> arbitrary <*> arbitrary
-- Using the new functionality
shrink (Pair x y) = toShrink (Pair <$> neShrink x <*> neShrink y)
This solution has both the conciseness of the original (and wrong) shrinkers,
while also giving the improved shrinks of either of the naive solutions, without
the downsides.
What can I do with this?
This library is licensed under the MIT license. See the LICENSE.md file for
details.