leancheck-0.9.3: doc/tutorial.md
Introduction to property-based testing (with LeanCheck)
=======================================================
This document introduces property-based testing. The reader only needs to be
familiar with Haskell. No previous knowledge of property-based testing is
assumed. This document focuses on LeanCheck, but skills are transferable to
[other property-based testing tools](#other-property-based-testing-tools-for-haskell).
(If you are already familiar with property-based testing and just want to learn
how to use LeanCheck, you might be best served by reading
[LeanCheck's README file](../README.md).
The learning outcomes of each section are:
* [What is property-based testing?](#what-is-property-based-testing)
--- what is property-based testing;
* [Example 1: testing `sort`](#example-1-testing-a-sort-implementation)
--- how to use a property-based testing library (LeanCheck);
* [Example 2: testing `insert`](#example-2-testing-conditional-properties)
--- how to test conditional properties
* [Example 3: testing `Stack`](#example-3-testing-user-defined-datatypes)
--- how to apply property-based testing to functions over user-defined
datatypes by declaring [`Listable`] typeclass instances;
What is property-based testing?
-------------------------------
In property-based testing, properties are defined as Haskell functions
returning a boolean value which should be `True` for all possible choices of
argument values. These properties are applied enumerated or random argument
values in search for a counterexample. This is perhaps better illustrated in
an example (see Example 1).
### Terminology
Property-based testing might be known with other names:
* property testing;
* [parameterized unit tests]: in the context of C# ([NUnit]) or Java ([JUnit]),
properties are viewed with unit tests with
arguments.
Example 1: testing a sort implementation
----------------------------------------
Lets imagine that we want to test an implementation of a (not-so-quick) `sort`
function:
sort :: Ord a => [a] -> [a]
sort [] = []
sort (x:xs) = sort lesser ++ [x] ++ sort greater
where
lesser = filter (< x) xs
greater = filter (> x) xs
### In contrast --- unit testing
We can *unit test* the above implementation:
testsPass :: Bool
testsPass = and
[ [] == sort ([]::[Int])
, [1] == sort [1]
, [1,2,3] == sort [1,2,3]
, [1,2,3] == sort [3,2,1]
, [1..100] == sort [100,99..1]
]
If we evaluate `testsPass` on ghci, we will get `True` --- our implementation
of `sort` passes all our unit tests.
### Declaring properties
Alternatively, we use *property-based testing* to test the above
implementation. We first declare a few properties:
prop_elem :: Ord a => a -> [a] -> Bool
prop_elem x xs = elem x (sort xs) == elem x xs
prop_ordered :: Ord a => [a] -> Bool
prop_ordered xs = ordered (sort xs)
where
ordered (x:y:xs) = x <= y && ordered (y:xs)
ordered _ = True
prop_length :: Ord a => [a] -> Bool
prop_length xs = length (sort xs) == length xs
Those properties are similar to unit tests, but are parameterized. Instead of
defining the behavior of sort for specific values, each property defines the
behavior of `sort` for a range of values.
### Testing properties
By binding those properties to specific types and passing those properties as
arguments to the [`check`] function, we get:
$ ghci
> import Test.LeanCheck
> check (prop_elem :: Int -> [Int] -> Bool)
+++ OK, passed 200 tests.
> check (prop_ordered :: [Int] -> Bool)
+++ OK, passed 200 tests.
Internally, the function [`check`] enumerates arguments to those functions and
test whether properties hold.
### Finding and fixing bugs
But what happens when the function does not follow the properties?
> check (prop_length :: [Int] -> Bool)
*** Failed! Falsifiable (after 3 tests):
[0,0]
The `check` function reports a failing counterexample. We get a `False` value
when evaluating `prop_length [0,0]`. We can investigate on GHCi:
> prop_length [0,0]
False
> length (sort [0,0]) == length [0,0]
False
> length (sort [0,0])
1
> sort [0,0]
[0]
If we look back at our definition of `sort`, we can see that we forgot to
account for repeated elements. We should change `>` to `>=`:
greater = filter (>= x) xs
After fixing that bug, `prop_length` will pass:
> check (prop_length :: [Int] -> Bool)
+++ OK, passed 200 tests.
_A little challenge:_
After `sort`ing a list, the number of occurrences of any element does not
change. Can you define a `prop_count :: Eq a => a -> [a] -> Bool` property
that checks this? Hint: start by defining an auxiliary function
`count :: a -> [a] -> Int`.
Example 2: testing conditional properties
-----------------------------------------
The boolean operator [`==>`] can be used to construct conditional properties.
The function [`insert`] defined in [`Data.List`] inserts an element into a list
at the first position where it is less than or equal to the next element.
*If the list is already ordered, the resulting list will still be ordered:*
prop_insertOrd x xs = ordered xs ==> ordered (insert x xs)
Example 3: testing user-defined datatypes
-----------------------------------------
Consider the following implementation of a `Stack`:
data Stack a = Stack a (Stack a)
| Empty
deriving (Show,Eq)
push :: a -> Stack a -> Stack a
push x s = Stack x s
pop :: Stack a -> (a, Stack a)
pop (Stack x s) = (x,s)
We might want to test the following property:
prop_popush :: a -> Stack a -> Bool
prop_popush x s = pop (push x s) == (x,s)
However, if we provide this property on ghci, we get an error:
> check (prop_popush :: Int -> Stack Int -> Bool)
<interactive>:x:1:
No instance for (Listable (Stack Int))
Our `Stack` type should be made an instance of the [`Listable`] typeclass.
This way LeanCheck will have a way to know how to list values to be tested by
the property. See [`Listable`] documentation for more. In this case, the
instance is:
instance Listable a => Listable (Stack a) where
tiers = cons2 Stack
\/ cons0 Empty
Now:
> check (prop_popush :: Int -> Stack Int -> Bool)
+++ OK, passed 200 tests.
LeanCheck also provides the function [`deriveListable`] to automatically derive
[`Listable`] instances for types that do not follow a data invariant (precondition).
The above [`Listable`] instance could be replaced by simply:
{-# LANGUAGE TemplateHaskell #-}
...
...
deriveListable ''Stack
Advantages of property-based testing
------------------------------------
Property-based testing has a few advantages over unit-testing:
* (+) scalability:
after making a small change to a program, we might [`checkFor`] `50` tests;
before making a major release, we may [`checkFor`] `1000` tests.
A continuous integration system can be configured to run more test than what
is usual on the development environment.
* (+) documentation:
properties serve as a clear documentation of behaviour;
* (+) tool support:
in Haskell there are several different property-based testing tools to choose
from.
The disadvantage is:
* (-) Properties are comparatively harder to write than simple input and output
test cases. (+) However, it might be easier to define good properties than a good
selection of unit test cases.
See "[Ranking programs using Black-Box testing (2010)]".
If you are unsure, you can always use *both* PBT and UT.
Writing a test program
----------------------
Writing a test program with LeanCheck is pretty simple. The following example
shows a full test program for the function `sort`.
import Test.LeanCheck
import Data.List (sort, elemIndices)
import System.Exit (exitFailure)
main :: IO ()
main =
case elemIndices False (tests 100) of
[] -> putStrLn "Tests passed!"
is -> putStrLn ("Failed tests:" ++ show is) >> exitFailure
-- given a maximum number of tests,
-- this function returns the test results
tests :: Int -> [Bool]
tests n =
[ holds n $ \xs -> ordered (sort xs :: [Int])
, holds n $ \xs -> length (sort xs :: [Int]) == length xs
, holds n $ \x xs -> (x `elem` (sort xs :: [Int])) == (x `elem` xs)
]
ordered :: Ord a => [a] -> Bool
ordered (x:y:xs) = x <= y && ordered (y:xs)
ordered _ = True
The above program prints the indices of failed tests if there are any.
The programmer can then copy-paste the selected test in GHCi to investigate.
The above program also returns an error code in case one of the tests fails
using `exitFailure`, so `make` or a CI system will detect that a test has
failed.
If you prefer a more heavyweight solution, LeanCheck has providers for the
Haskell testing frameworks [Tasty], [test-framework] and [Hspec]:
* [LeanCheck provider for Tasty]
-- `$ cabal install tasty-leancheck` ;
* [LeanCheck provider for test-framework]
-- `$ cabal install test-framework-leancheck` ;
* [LeanCheck provider for Hspec]
-- `$ cabal install hspec-leancheck` .
Other property-based testing tools for Haskell
----------------------------------------------
* [QuickCheck] : randomized
* [Hedgehog] : randomized
* [SmallCheck] : enumerative, depth-bounded
* [Lazy SmallCheck] : enumerative, depth-bounded, lazy, demand-driven
* [Feat] : enumerative, size-bounded
* [LeanCheck] : enumerative, size-bounded
Further reading
---------------
* [Using LeanCheck on functions over types with a data invariant](data-invariant.md)
* [Testing and tracing using QuickCheck and Hat](https://www.cs.kent.ac.uk/pubs/2003/1896/content.pdf)
* [QuickCheck's seminal paper (2000)](https://dl.acm.org/citation.cfm?id=1988046)
* [SmallCheck's paper (2008)](http://dl.acm.org/citation.cfm?id=1411292)
[`Listable`]: https://hackage.haskell.org/package/leancheck/docs/Test-LeanCheck.html#t:Listable
[`check`]: https://hackage.haskell.org/package/leancheck/docs/Test-LeanCheck.html#v:check
[`checkFor`]: https://hackage.haskell.org/package/leancheck/docs/Test-LeanCheck.html#v:checkFor
[`==>`]: https://hackage.haskell.org/package/leancheck/docs/Test-LeanCheck.html#v:-61--61--62-
[`deriveListable`]: https://hackage.haskell.org/package/leancheck/docs/Test-LeanCheck.html#v:deriveListable
[QuickCheck]: https://hackage.haskell.org/package/QuickCheck
[Hedgehog]: https://hackage.haskell.org/package/hedgehog
[SmallCheck]: https://hackage.haskell.org/package/smallcheck
[Lazy SmallCheck]: https://hackage.haskell.org/package/lazysmallcheck
[Feat]: https://hackage.haskell.org/package/testing-feat
[LeanCheck]: https://hackage.haskell.org/package/leancheck
[parameterized unit tests]: http://research.microsoft.com/apps/pubs/default.aspx?id=77419
[NUnit]: http://www.nunit.org/index.php?p=parameterizedTests&r=2.5
[JUnit]: http://www.mkyong.com/unittest/junit-4-tutorial-6-parameterized-test/
[`insert`]: https://hackage.haskell.org/package/base/docs/Data-List.html#v:insert
[`Data.List`]: https://hackage.haskell.org/package/base/docs/Data-List.html
[Ranking programs using Black-Box testing (2010)]: http://www.cse.chalmers.se/~nicsma/papers/ranking-programs.pdf
[Tasty]: https://github.com/feuerbach/tasty#readme
[test-framework]: https://haskell.github.io/test-framework/
[Hspec]: https://hspec.github.io/
[LeanCheck provider for Tasty]: https://hackage.haskell.org/package/tasty-leancheck
[LeanCheck provider for test-framework]: https://hackage.haskell.org/package/test-framework-leancheck
[LeanCheck provider for Hspec]: https://hackage.haskell.org/package/hspec-leancheck