I would like to generate random terms based on some sort of "context" and I was wondering if this is possible using quickcheck. Basically I would like to have an additional data type passed around so that the arbitrary function can generate terms based on the additional parameter... Is this possible with quickcheck or should I just write my own definition of Gen?
2 Answers
It's possible, though not really sane, to do this from within arbitrary
. But if you step out of arbitrary
, you can literally just pass an extra parameter around.
-- do whatever you want inside the implementation of these two
chooseIntRange :: Context -> Int
updateContext :: Int -> Context -> Context
arbitraryIntWithContext :: Context -> Gen (Context, Int)
arbitraryIntWithContext ctx = do
n <- choose (0, chooseIntRange ctx)
return (n, updateContext n ctx)
The plumbing of the context can be relieved somewhat with StateT
, e.g.
-- do whatever you want inside the implementation of this
chooseIntRangeAndUpdate :: MonadState Context m => m Int
arbitraryIntStateT :: StateT Context Gen Int
arbitraryIntStateT = do
hi <- chooseIntRangeAndUpdate
lift (choose (0, hi))
While Daniel Wagner has supplied a fine answer for QuickCheck (+1), it also highlights one of QuickCheck's weaknesses. In QuickCheck, one writes properties using instances of Arbitrary
, but due to its design, Arbitrary
isn't monadic.
The workaround that Daniel Wagner shares is that Gen
, on the other hand, is monadic, so that you can write context-dependent code using do
notation. The disadvantage is that while you can convert a Gen a
to an Arbitrary a
, you'll either have to provide a custom shrink
implementation, or forgo shrinking.
An alternative library for property-based testing, Hedgehog, is designed in such a way that properties themselves are monadic, which means you'd be able to write an entire property and simply embed ad-hoc context-specific value generation (including shrinking) in the test code itself:
propWithContext :: Property
propWithContext = property $ do
ctx <- forAll genContext
n <- forAll $ Gen.integral $ Range.linear 0 $ chooseIntRange ctx
let ctx' = updateContext n ctx
-- Exercise SUT and verify result here...
Here, genContext
is a custom generator for the Context
type, with the type
genContext :: MonadGen m => m Context