quickcheck
quickcheck copied to clipboard
RFC: Create a SuchThat data type that allows call site `suchThat`
I was recently writing tests where I needed some properties to hold in a specific situation. My usual path here is to make a newtype to get the Arbitrary instance I want. However I realized that suchThat could be used at the call site via Testable. What do you think of this type?
data SuchThat a prop = SuchThat (a -> Bool) (a -> prop)
instance (Arbitrary a, Show a, Testable prop) => Testable (SuchThat a prop) where
property (SuchThat predicate prop) =
forAllShrink (arbitrary `suchThat` predicate) shrink prop
This allows Arbitrary instances to be decorated via a generic combinator at the call site.
Here is a trivial example that would always pass:
it "is greater than 0" . property . SuchThat (> 0) $ \int -> int > 0
Nice one!
I like it!
One comment: it could instead be a property combinator in the same style as e.g. forAll:
??? :: (Arbitrary a, Testable prop) => (a -> Bool) -> (a -> prop) -> Property
However, the name suchThat is already taken, and I'm not sure what a good available name would be.
Another comment: it should also check the predicate during shrinking (i.e. shrink should be filter predicate . shrink).
forAllSuchThat, forAllSuchThatShrink, forAllSuchThatShrinkShow? Those are mouthful, but are consistent
I like it! If we had this, would we still have a need for (==>)? Not suggesting removal, but maybe favoring SuchThat/forAllSuchThat/... in docs?
@tom-bop it's matter of taste which is nicer:
it "is greater than 0" . property . SuchThat (> 0) $ \int -> int > 0
it "is greater than 0" . property . $ \int -> int > 0 ==> int > 0
I think the disadvantage of (==>) is that it discards.
E.g. if this runs 100 tests:
quickCheck $ SuchThat even $ \i -> ...
This will only run (i.e. not skip) ~50:
quickCheck $ \i -> even i ==> ...
Would this be the recommended way to handle more than one test argument?:
quickCheck $ SuchThat (\(i0, i1) -> even i0 && odd i1) $ \(i0, i1) -> i0 /= i1
I think the disadvantage of (==>) is that it discards.
E.g. if this runs 100 tests:
quickCheck $ SuchThat even $ \i -> ...
This will only run (i.e. not skip) ~50:
quickCheck $ \i -> even i ==> ...
No, QuickCheck continues to run tests until it reaches (by default) 100 non-skipped tests.
The difference between ==> and suchThat is when you have constraints on multiple arguments:
-
\x y -> p x && q y ==> ... -
SuchThat p $\x -> SuchThat q $ \y -> ...
(1) will generate a whole test case (x and y), then throw it out if the precondition does not hold. (2) will generate x and y independently (when generating y, if q y does not hold it will not retry generating x). (2) will be quicker to succeed if there are lots of independent conditions on different test arguments (since (1) has to satisfy them all simultaneously), but it also has the risk of hanging if it makes a choice of x which makes it impossible to satisfy q y.