You want to ensure that your code behaves in a particular way; the simplest way to check the behaviour of code is to test it.
In this case, the desired behaviour is that each constructor gets reasonable coverage in tests. We can check that with a simple test:
allCons xs = length xs > 100 ==> length constructors == 3
where constructors = nubBy eqCons xs
eqCons C1 C1 = True
eqCons C1 _ = False
eqCons (C2 _) (C2 _) = True
eqCons (C2 _) _ = False
eqCons (C3 _ _) (C3 _ _) = True
eqCons (C3 _ _) _ = False
This is pretty naive, but it's a good first shot. Its advantages:
eqCons
will trigger an exhaustiveness warning if new constructors are added, which is what you want
- It checks that your instance is handling all constructors, which is what you want
- It also checks that all constructors are actually generated with some useful probability (in this case at least 1%)
- It also checks that your instance is usable, eg. doesn't hang
Its disadvantages:
- Requires a large amount of test data, in order to filter out those with length > 100
eqCons
is quite verbose, since a catch-all eqCons _ _ = False
would bypass the exhaustiveness check
- Uses magic numbers 100 and 3
- Not very generic
There are ways to improve this, eg. we can compute the constructors using the Data.Data module:
allCons xs = sufficient ==> length constructors == consCount
where sufficient = length xs > 100 * consCount
constructors = length . nub . map toConstr $ xs
consCount = dataTypeConstrs (head xs)
This loses the compile-time exhaustiveness check, but it's redundant as long as we test regularly and our code has become more generic.
If we really want the exhaustiveness check, there are a few places where we could shoe-horn it back in:
allCons xs = sufficient ==> length constructors == consCount
where sufficient = length xs > 100 * consCount
constructors = length . nub . map toConstr $ xs
consCount = length . dataTypeConstrs $ case head xs of
x@(C1) -> x
x@(C2 _) -> x
x@(C3 _ _) -> x
Notice that we use consCount to eliminate the magic 3
completely. The magic 100
(which determined the minimum required frequency of a constructor) now scales with consCount, but that just requires even more test data!
We can solve that quite easily using a newtype:
consCount = length (dataTypeConstrs C1)
newtype MyTypeList = MTL [MyType] deriving (Eq,Show)
instance Arbitrary MyTypeList where
arbitrary = MTL <$> vectorOf (100 * consCount) arbitrary
shrink (MTL xs) = MTL (shrink <$> xs)
allCons (MTL xs) = length constructors == consCount
where constructors = length . nub . map toConstr $ xs
We can put a simple exhaustiveness check in there somewhere if we like, eg.
instance Arbitrary MyTypeList where
arbitrary = do x <- arbitrary
MTL <$> vectorOf (100 * consCount) getT
where getT = do x <- arbitrary
return $ case x of
C1 -> x
C2 _ -> x
C3 _ _ -> x
shrink (MTL xs) = MTL (shrink <$> xs)
C2{}
instead ofC2 _
and so on which at least makes the syntax a bit nicer. – Degaussundefined
thing will fail if the constructor is strict. – HollenbecksomeCustomGen
. If I have some invariant not captured by the type system, or otherwise want to influence with what probability alternatives are generated (to make it more likely to generate non-trivial cases), I have to customise parts of the Arbitrary instance (e.g. with QuickCheck'sfrequency
function). – Degauss