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!

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 `Bool`

to `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]

In fact, `(-=-)`

is not directly defined in terms of `test`

but in terms of another specialized combinator.

(-=-) :: a -> a -> Prop x -=- y = (x,y) `is` uncurry (==)

The combinator `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 `reverse`

over `(++)`

, … 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 `(-=-)`

is `[False,True]`

, that's ok. The result of the right side of `(-=-)`

is `[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.

Passed! `foldl (flip (:)) []`

does the trick.

Not every Curry operation is deterministic. We have seen the combinators `is`

and `(-=-)`

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

Remember `reverseOfNonEmptyIsNotNull`

? We may want to define a similar property for `insert`

.

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 [1] 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.- Finally,
`(<~>)`

demands the sets of results of the arguments to be*equal*.

Note, that `(<~>)`

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 `(~>)`

and `(<~)`

? Isn't one just the flipped version of the other? Semantically, yes. Operationally, no. EasyCheck reports failing tests differently for `(~>)`

and `(<~)`

. 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 [1] 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 [1] 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 `(~>)`

, `(<~)`

or `(<~>)`

. If this seems suspicious to you, reconsider the definition of `(-=-)`

. Why did we define

x -=- y = (x,y) `is` uncurry (==)

instead of

x -=- y = x `is` (==y)

and why is this only possible for deterministic equality?

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!