I have a QuickCheck property testing a function f
. The property maps the function f
over some list xs
and checks some element-wise property of the result. In the case of failure, I'd like to display the element of xs
related to this failure. Consider the following property:
prop x =
printTestCase ("Failed for value " ++ show failure) $ isNothing failure
where
failure = fmap fst $ find (not . snd) $ map (\n -> (n, f x n == n)) [10..20]
This works fine for the implementation
f = (+)
and quickcheck prop
outputs
*** Failed! Falsifiable (after 2 tests):
1
Failed for value Just 10
However, if f
throws an exception, i.e.
f = undefined
then quickcheck prop
outputs
*** Failed! Exception: 'Prelude.undefined' (after 1 test):
()
Failed for value Exception thrown by generator: 'Prelude.undefined'
How can I write a property which catches this second exception and returns "Just 0"
as for the previous example? I guess, one could use whenFail
or whenFail'
for this, but I haven't yet understood the QuickCheck internals.
Thanks to aavogt on the #haskell channel, I found the module Control.Spoon
, which provides the functions spoon and teaspoon which can be used to solve this problem. However, I don't know how safe it is to use this package (which uses unsafePerformIO internally
).
Anyway, using Control.Spoon
, prop
can be rewritten the following way:
prop x =
printTestCase ("Failed for value " ++ show pureFailure) $ isNothing excFailure
where
pureFailure = failure (fromMaybe True . teaspoon)
excFailure = failure id
failure g = fmap fst $ find (g . not . snd) $ map (\n -> (n, f x n == n)) [10..20]
Wrapping the find predicate with fromMaybe True . teaspoon
has the effect that find returns
the first element either satisfying the predicate or throwing an exception.
As teaspoon
can only be used to see that an exception occured, but not which, I use
excFailure
here so that QuickCheck still sees the exception. The downside is that the find
needs to be evaluated twice. teaspoon
only evaluates to WHNF, if you need a deep evaluation to trigger the exception, use spoon
instead.
teaspoon
catches only a few select exceptions, which is fine for me, because I only really care about undefined and inexhaustive pattern matches.