24 Days of Hackage: QuickCheck

As I mentioned in the article on errors, the Haskell programmer takes error handling and edge cases very seriously. While we try and constrain our types as much as possible, there is always a trade off between exact types and pragmatism, not to mention that there are some invariants that are very difficult to encode in the Haskell type system. As such, without rigorous testing, there is still a risk of exceptions or unexpected behaviors at runtime.

The solution is, of course - testing! And if we really want to be confident in our application, we need to be certain we have tested it under all possible inputs. Again, there is a trade off to be made - you could logically reason about your program, proving things by induction and so on, but this is a demanding task, and one that goes beyond a lot of programmers abilities. The solution most people are used to, is to manually generate some test data that exercises different aspects of a system, and hope it’s good enough. While you can do that in Haskell, there’s also a third option.

QuickCheck is a library for doing random testing. This means that rather than have the programmer write test data, QuickCheck will generate random data for you. It sounds naive, doesn’t it? It’s a simple solution, but an incredibly powerful one - in fact many people swear by it.

Let’s dig in with an example!

absAverage :: [Double] -> Double
absAverage ds = sum ds / fromIntegral (length ds)

My intention with this function was to take the average of the absolute value of all values in a list. So, lets write a property to make sure this is correct:

quickCheck1 :: IO ()
quickCheck1 = quickCheck $ \x -> absAverage x >= 0

> quickCheck1
*** Failed! Falsifiable (after 1 test):
[]

Here I’ve asserted the property that “for all lists of integers, x, absAverage x is positive.” But QuickCheck made light work of that property and quickly proved us wrong! QuickCheck generated some test data for us - in this case the empty list - and our property didn’t hold - because our function doesn’t make sense for empty lists. We can weaken the property a bit to only consider non-empty lists:

quickCheck2 :: IO ()
quickCheck2 = quickCheck $ \x -> length x > 1 ==> absAverage x >= 0

> quickCheck2
*** Failed! Falsifiable (after 2 tests and 3 shrinks):
[-2.0,1.0]

Huh, a failure again… Oh! I forgot to actually take the abs value of each element of ds in my original function, lets get that fixed…

absAverage :: [Double] -> Double
absAverage ds = sum (map abs ds) / fromIntegral (length ds)

> quickCheck2
+++ OK, passed 100 tests

Alright!

QuickCheck can do a lot more than this, but in the spirit of these articles I’m only trying to scratch the surface - it’s up to you to do the extra reading. Thankfuly, there’s a lot of great material already published. Check out the introduction to QuickCheck on the Haskell wiki, or the excellent chapter on Haskell testing in Real World Haskell.

It’s worth noting that QuickCheck is not the only library of this ilk; the smallcheck operates under a similar principle, however smallcheck tries to build random data of various ‘depth’, based on the assumption that “If a program fails to meet its specification in some cases, it almost always fails in some simple case.”

Now that you have QuickCheck you don’t even have to think about generating data for your tests, and you can concentrate on the essential properties. In other words - no excuses!


You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

I accept Bitcoin donations: 14SsYeM3dmcUxj3cLz7JBQnhNdhg7dUiJn