Table of Contents
EasyCheck is a library for automated, specification-based testing of Curry programs. It is distributed with the Curry implementation KiCS. The ideas behind EasyCheck are described in this paper. This is a tutorial introduction that explains in detail how to use EasyCheck.
In EasyCheck, you define properties for functions that describe their behaviour and EasyCheck automatically checks these properties for a variety of test data. Properties are values of the abstract datatype
Prop and can be constructed by the following combinator.
test :: a -> ([a] -> Bool) -> Prop
This combinator takes an arbitrary value as argument and a predicate that is applied to the list of nondeterministic results of this value. The given predicate determines whether the constructed property is satisfied or falsified for the given value.
There are numerous specialized combinators defined in terms of
test and we will get to know all of them in the rest of this tutorial. Usually, you will use one of the specialized combinators and not
test in order to specify properties. Of course, nobody prevents you from using
test directly, if you prefer!
Testing Deterministic Operations
Although Curry is a functional logic language that supports nondeterminism, many Curry operations are deterministic. For example, you may have defined an operation that reverses lists as follows.
reverse :: [a] -> [a] reverse = foldl (flip (:)) 
Now you doubt whether this is indeed a correct definition of the reverse function and wonder how you could check this. For example,
reverse should leave a singleton list untouched, so you recall the above
test combinator, import EasyCheck and define a corresponding property.
import EasyCheck reverse :: [a] -> [a] reverse = foldl (flip (:))  reverseLeavesSingletonUntouched :: a -> Prop reverseLeavesSingletonUntouched x = test (reverse [x]) valid where valid results = case results of [[y]] -> x == y _ -> False
This is not exactly a beautiful definition, but it will become nicer soon. First, let's try to check the property with EasyCheck. We need to enable OrBased mode in order to be able to generate test data automatically. We use the operation
easyCheck1 because our property takes one argument.
> :set or > easyCheck1 reverseLeavesSingletonUntouched CurryRequest.hs:14:39: Ambiguous type variable `t0' in the constraint: `Curry t0' arising from use of `c_easyCheck1' at CurryRequest.hs:14:39-69 Probable fix: add a type signature that fixes these type variable(s)
This is a kind of weird error message, but KiCS gurus can tell you the reason: EasyCheck does not know what test data to generate because the type of the property is polymorphic. We need to specialize the type, in order to enable EasyCheck to construct test data and decide to use
Bool as element type for the lists passed to reverse.
reverseLeavesSingletonUntouched :: Bool -> Prop
Now, EasyCheck succeeds to verify our property.
> easyCheck1 reverseLeavesSingletonUntouched Passed 2 tests.
As there are only two booleans to check, we did even prove the property! Let's change the element type from
Int in order to see what EasyCheck does with an infinite amount of test data.
reverseLeavesSingletonUntouched :: Int -> Prop
Observe the counter and see how EasyCheck finally reports 1000 successful tests.
> easyCheck1 reverseLeavesSingletonUntouched Ok, passed 1000 tests.
As reverse does not touch elements, we could already have been confident with the first test. But 1000 looks much more impressive than 2, doesn't it? This is our first successful check but many more will follow and, be warned, you may get addicted to them ;)
Remember that I said you usually won't use
test directly? Now, it's time to refurbish our property. EasyCheck provides a combinator to check for equality of deterministic values.
(-=-) :: a -> a -> Prop
If we use it in the definition of
reverseLeavesSingletonUntouched, the definition becomes much clearer.
reverseLeavesSingletonUntouched :: Bool -> Prop reverseLeavesSingletonUntouched x = reverse [x] -=- [x]
(-=-) is not directly defined in terms of
test but in terms of another specialized combinator.
(-=-) :: a -> a -> Prop x -=- y = (x,y) `is` uncurry (==)
is is similar to
test but demands its first argument to be deterministic.
is :: a -> (a -> Bool) -> Prop x `is` ok = test x valid where valid results = case results of [y] -> ok y _ -> False
We can use
is to define another property for reverse.
reverseOfNonemptyIsNotNull :: Bool -> [Bool] -> Prop reverseOfNonemptyIsNotNull x xs = reverse (x:xs) `is` (not . null)
This time, we use
easyCheck2 to check our property with now two arguments.
> easyCheck2 reverseOfNonemptyIsNotNull OK, passed 1000 tests.
Successful again! But to be honest, these properties don't rock - they would even hold for the identity function. Let's come up with a more interesting property. Hmm, … there was something about distribution of
(++), … let's try …
reverseDistributesOverAppend :: [Bool] -> [Bool] -> Prop reverseDistributesOverAppend xs ys = reverse (xs++ys) -=- reverse xs ++ reverse ys
Does this property hold?
> easyCheck2 reverseDistributesOverAppend Falsified by 8th test. Arguments: [True] [False] Results: ([False,True],[True,False])
Ouch.. It doesn't. Let's take a closer look. The 8th test falsifies our property. It was called with
xs = [True] and
ys = [False]. The result of the left side of
[False,True], that's ok. The result of the right side of
[True,False], that's ok as well. Never blindly trust your properties! We did not expose an error in
reverse but in the property used to test it. The correct property should look like this.
reverseAntiDistributesOverAppend :: [Bool] -> [Bool] -> Prop reverseAntiDistributesOverAppend xs ys = reverse (xs++ys) -=- reverse ys ++ reverse xs
Now that we are confident that the property is correct we can go back to check whether our definition of
reverse is correct also.
> easyCheck2 reverseAntiDistributesOverAppend OK, passed 1000 tests.
foldl (flip (:))  does the trick.
Testing Nondeterministic Operations
Not every Curry operation is deterministic. We have seen the combinators
(-=-) for testing deterministic operations and will examine their counterparts for testing nondeterministic operations in this part of the tutorial.
In our examples we will check the definition of an operation that inserts an element into a list at an arbitrary position.
insert :: a -> [a] -> [a] insert x xs = x : xs insert x (y:ys) = y : insert x ys
reverseOfNonEmptyIsNotNull? We may want to define a similar property for
insertIsNotNull :: Int -> [Int] -> Prop insertIsNotNull x xs = insert x xs `is` (not . null)
If we check this property with EasyCheck, we get the following counterexample.
> easyCheck2 insertIsNotNull Falsified by second test. Arguments: 0  Results: [0,1] [1,0]
The results are not empty, but there are two of them. The combinator
is demands its first argument to be deterministic and fails for nondeterministic values. What do we mean by this property anyway? We need to state whether we want the predicate
not . null to hold for all results or only for some result of the call to
insert. The following combinators let us express what we mean.
isAlways, isEventually :: a -> (a -> Bool) -> Prop isAlways x = test x . all isEventually x = test x . any
Now we can restate our property as follows.
insertIsNeverNull :: Int -> [Int] -> Prop insertIsNeverNull x xs = insert x xs `isAlways` (not . null)
Just to be safe, we ask EasyCheck to verify our property.
> easyCheck2 insertIsNeverNull OK, passed 1000 tests.
What are the counterparts to
(-=-)? What different kinds of equality checks are there for nondeterministic operations? EasyCheck provides three of them.
(~>), (<~), (<~>) :: a -> a -> Prop
You may think of them as reduction arrows to feel the difference.
(~>)demands that its first argument evalautes to every value of its second argument. The set of results of the first argument must be a (not necessarily strict) superset of the set of results of the second argument.
(<~)is dual to
(~>)and demands that the set of results of the first argument is a subset of the set of results of the second argument.
(<~>)demands the sets of results of the arguments to be equal.
(<~>) is not equivalent to
(-=-) because the latter demands its arguments to be deterministic.
Now we can specify that insert should yield a list where the given element is inserted as first element.
insertInsertsAsHead :: Int -> [Int] -> Prop insertInsertsAsHead x xs = insert x xs ~> x:xs
We collect more and more successful tests..
> easyCheck2 insertInsertsAsHead OK, passed 1000 tests.
Why do we need both
(<~)? Isn't one just the flipped version of the other? Semantically, yes. Operationally, no. EasyCheck reports failing tests differently for
(<~). Consider the following - intentionally false - property.
insertIsConsOrFails :: Int -> [Int] -> Prop insertIsConsOrFails x xs = x:xs ~> insert x xs
It states that
x:xs evaluates to all values of
insert x xs, i.e.,
insert is the same as
(:) or fails. If we use EasyCheck to check this property, we get the following counterexample.
> easyCheck2 insertIsConsOrFails Falsified by second test. Arguments: 0  Results: [0,1]
EasyCheck reports the results of the left argument of
(~>) but not those of the right argument. If we want to see, why the property fails, we need to restate it using
insertIsConsOrFails :: Int -> [Int] -> Prop insertIsConsOrFails x xs = insert x xs <~ x:xs
Now the counterexample is much clearer.
> easyCheck2 insertIsConsOrFails Falsified by second test. Arguments: 0  Results: [0,1] [1,0]
The same holds for
(<~>). You should always write the expression you want to observe in case of a failing test on the left side of
(<~>). If this seems suspicious to you, reconsider the definition of
(-=-). Why did we define
x -=- y = (x,y) `is` uncurry (==)
x -=- y = x `is` (==y)
and why is this only possible for deterministic equality?
Test Data Distribution
Constrained Free Variables
This part is meant as quick reference that you can use if you already read the tutorial and only want to see the most important combinators at a glance.
to be completed..
Interested in the search employed by EasyCheck and experiments on test-data distribution? Check out our paper!