[Published in Open Source For You (OSFY) magazine, February 2015 edition.]
Let’s take a look at the property-based testing of Haskell programs and at the Cabal tool, which is used to build and manage Haskell packages and applications.
One of the main features of testing in Haskell is property-based testing. The type system allows you to infer and derive types, and also helps in auto-generating test cases. QuickCheck is a popular property-based testing library for Haskell. If your program is pure, you can write tests to ascertain the properties and invariants of your programs, and the tests can be auto-generated and executed.
You can install QuickCheck on Fedora, for example, by using the following command:
$ sudo yum install ghc-QuickCheck-devel
Consider a simple function to add two integers:
mySum :: Int -> Int -> Int
mySum a b = a + b
We can ascertain the property of the function that ‘a + b’ is the same as ‘b + a’ using the QuickCheck library. You must first define the invariant in a function as shown below:
prop_mySum a b = mySum a b == mySum b a
You can test the code directly in the GHCi prompt, using the following command:
$ ghci sum.hs
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( sum.hs, interpreted )
Ok, modules loaded: Main.
ghci> prop_mySum 2 3
Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package old-locale-1.0.0.5 ... linking ... done.
Loading package time-1.4.0.1 ... linking ... done.
Loading package random-1.0.1.1 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Loading package pretty-1.1.1.0 ... linking ... done.
Loading package template-haskell ... linking ... done.
Loading package QuickCheck-2.6 ... linking ... done.
True
You can also invoke the quickCheck function in a main function, as shown below:
import Test.QuickCheck
mySum :: Int -> Int -> Int
mySum a b = a + b
prop_mySum :: Int -> Int -> Bool
prop_mySum a b = mySum a b == mySum b a
main :: IO ()
main = quickCheck prop_mySum
Compiling and executing the above code produces the following output:
$ ghc --make sum.hs
[1 of 1] Compiling Main ( sum.hs, sum.o )
Linking sum ...
$ ./sum
+++ OK, passed 100 tests.
You can also dump the input that was generated for the various test cases using the verboseCheck function, as shown below:
main :: IO ()
main = verboseCheck prop_mySum
Executing the above code with the updated main function will yield 100 input test cases that were generated in runtime.
ghci> main
Passed:
0
0
Passed:
-1
1
Passed:
64
-44
Passed:
-2159
2134
Passed:
-927480859
61832343
...
The head function in Haskell expects to receive a non-empty list. You can write a headExists function to check if the head exists for a list of integers, as shown below:
headExists :: [Int] -> Bool
headExists list
| null list = False
| otherwise = True
You can load the above code in GHCi and test it out, as follows:
ghci> headExists []
False
ghci> headExists [1, 2, 3]
True
Let’s assume that, by mistake, you wrote an incorrect property-based test where the headExists function will always return ‘False’, ignoring the ‘otherwise’ case.
import Test.QuickCheck
headExists :: [Int] -> Bool
headExists list
| null list = False
| otherwise = True
prop_headExists :: [Int] -> Bool
prop_headExists emptyList = headExists emptyList == False
main :: IO ()
main = quickCheck prop_headExists
Testing the code produces the following output:
$ ghc --make head.hs
[1 of 1] Compiling Main ( head.hs, head.o )
Linking head ...
$ ./head
*** Failed! Falsifiable (after 3 tests):
[0]
The QuickCheck library generated test cases for different [Int] types and it returned a failure after the third test, for which the input was [0]. Clearly, the ‘headExists [0]’ computation will return ‘True’ and not ‘False’.
The way we defined the property is incorrect. We know that if the list is empty, then its length is zero. We can write a helper function lengthZero for the above, as follows:
lengthZero :: [Int] -> Bool
lengthZero list
| length list == 0 = True
| otherwise = False
We can then use this function to assert that for any Integer list, if headExists returns ‘False’ then the lengthZero function must return ‘True’. The complete code is shown below:
import Data.List
import Test.QuickCheck
headExists :: [Int] -> Bool
headExists list
| null list = False
| otherwise = True
lengthZero :: [Int] -> Bool
lengthZero list
| length list == 0 = True
| otherwise = False
prop_headExists :: [Int] -> Bool
prop_headExists list = headExists list == not (lengthZero list)
main :: IO ()
main = quickCheck prop_headExists
Executing the code produces the required output:
$ ghc --make head.hs
[1 of 1] Compiling Main ( head.hs, head.o )
Linking head ...
$ ./head
+++ OK, passed 100 tests.
We can also re-write the above code based on conditional properties. The property that the headExists function will return ‘True’ only for non-empty lists can be defined as a constraint. The notation syntax is condition ==> property. In our example, if the condition that the list is non-empty is ‘True’, then the property that the headExists function for the list must return is ‘True’. Also, when the list is empty, the headExists function must return ‘False’. These two conditions can be written as follows:
import Data.List
import Test.QuickCheck
headExists :: [Int] -> Bool
headExists list
| null list = False
| otherwise = True
prop_headExists :: [Int] -> Property
prop_headExists list = length list > 0 ==> headExists list == True
prop_emptyList :: [Int] -> Property
prop_emptyList list = length list == 0 ==> headExists list == False
main :: IO ()
main = do
quickCheck prop_headExists
quickCheck prop_emptyList
Testing the code produces the following output:
$ ghc --make cond.hs
[1 of 1] Compiling Main ( cond.hs, cond.o )
Linking cond ...
$ ./cond
+++ OK, passed 100 tests.
*** Gave up! Passed only 38 tests.
These tests can be integrated with Hspec or HUnit for a more verbose output.
Cabal
Cabal is a software tool that is used to describe a Haskell application, list its dependencies, and provide a manifestation to distribute the source and binaries. It is not to be confused with a distribution package manager like RPM or the Debian package management system. You can install Cabal using your distribution package manager. On Fedora, for example, you can use the following command:
$ sudo yum install cabal-install
Haskell software programs are available in hackage.haskell.org, and each project has a .cabal file. Let us take an example of the HSH-2.1.2 package at http://hackage.haskell.org/package/HSH which allows you to use shell commands and expressions within Haskell programs. You can download HSH-2.1.2.tar.gz and extract it using:
$ tar xzvf HSH-2.1.2.tar.gz
HSH-2.1.2/
HSH-2.1.2/COPYING
HSH-2.1.2/HSH.cabal
HSH-2.1.2/testsrc/
HSH-2.1.2/testsrc/runtests.hs
HSH-2.1.2/HSH.hs
HSH-2.1.2/HSH/
HSH-2.1.2/HSH/Command.hs
HSH-2.1.2/HSH/ShellEquivs.hs
HSH-2.1.2/HSH/Channel.hs
HSH-2.1.2/COPYRIGHT
HSH-2.1.2/Setup.lhs
The .cabal file has various fields that describe the Haskell application. The contents of the HSH.cabal for version 2.1.2 are given below:
Name: HSH
Version: 2.1.3
License: LGPL
Maintainer: John Goerzen <jgoerzen@complete.org>
Author: John Goerzen
Stability: Beta
Copyright: Copyright (c) 2006-2014 John Goerzen
Category: system
license-file: COPYRIGHT
extra-source-files: COPYING
homepage: http://software.complete.org/hsh
Synopsis: Library to mix shell scripting with Haskell programs
Description: HSH is designed to let you mix and match shell expressions with
Haskell programs. With HSH, it is possible to easily run shell
commands, capture their output or provide their input, and pipe them
to and from other shell commands and arbitrary Haskell functions at will.
Category: System
Cabal-Version: >=1.2.3
Build-type: Simple
flag buildtests
description: Build the executable to run unit tests
default: False
library
Exposed-Modules: HSH, HSH.Command, HSH.ShellEquivs, HSH.Channel
Extensions: ExistentialQuantification, OverlappingInstances,
UndecidableInstances, FlexibleContexts, CPP
Build-Depends: base >= 4 && < 5, mtl, process, regex-compat, MissingH>=1.0.0,
hslogger, filepath, regex-base, regex-posix, directory,
bytestring
if !os(windows)
Build-Depends: unix
GHC-Options: -O2 -threaded -Wall
Executable runtests
if flag(buildtests)
Buildable: True
Build-Depends: base >= 4 && < 5, mtl, process, regex-compat,
MissingH>=1.0.0,
hslogger, filepath, regex-base, regex-posix, directory,
bytestring, HUnit, testpack
if !os(windows)
Build-Depends: unix
else
Buildable: False
Main-Is: runtests.hs
HS-Source-Dirs: testsrc, .
Extensions: ExistentialQuantification, OverlappingInstances,
UndecidableInstances, FlexibleContexts, CPP
GHC-Options: -O2 -threaded
Enter the HSH-2.1.2 directory and configure the project using the cabal configure command as shown below:
$ cd HSH-2.1.2
$ cabal configure
Resolving dependencies...
Configuring HSH-2.1.2...
You can then compile the project sources using the cabal build step:
$ cabal build
Building HSH-2.1.2...
Preprocessing library HSH-2.1.2...
[1 of 4] Compiling HSH.Channel ( HSH/Channel.hs, dist/build/HSH/Channel.o )
...
[2 of 4] Compiling HSH.Command ( HSH/Command.hs, dist/build/HSH/Command.o )
...
[3 of 4] Compiling HSH.ShellEquivs ( HSH/ShellEquivs.hs, dist/build/HSH/ShellEquivs.o )
...
[4 of 4] Compiling HSH ( HSH.hs, dist/build/HSH.o )
In-place registering HSH-2.1.2...
You can install the built library files using the cabal install command. By default, it installs to ~/.cabal folder as shown below:
$ cabal install
Resolving dependencies...
Configuring HSH-2.1.2...
Building HSH-2.1.2...
Preprocessing library HSH-2.1.2...
In-place registering HSH-2.1.2...
Installing library in /home/guest/.cabal/lib/HSH-2.1.2/ghc-7.6.3
Registering HSH-2.1.2...
Installed HSH-2.1.2
You can also generate HTML documentation for the source code using the cabal haddock option. The HTML files can also be made available at hackage.haskell.org.
$ cabal haddock
Running Haddock for HSH-2.1.2...
Preprocessing library HSH-2.1.2...
Warning: The documentation for the following packages are not installed. No
links will be generated to these packages: MissingH-1.3.0.1, rts-1.0,
hslogger-1.2.6, network-2.6.0.2
Haddock coverage:
...
Documentation created: dist/doc/html/HSH/index.html
If you make changes to the sources and wish to generate a new release, you can update the Version field in the HSH.cabal file.
Version: 2.1.3
In order to make a new tarball, use the cabal sdist command:
$ cabal sdist
Distribution quality warnings:
...
Building source dist for HSH-2.1.3...
Preprocessing library HSH-2.1.3...
Source tarball created: dist/HSH-2.1.3.tar.gz
To test the installed application, you can run GHCi from a directory other than the HSH-2.1.2 sources directory. For example:
$ ghci
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
ghci> :m + HSH
ghci HSH> runIO "date"
Sun Jan 4 14:22:37 IST 2015
You should not run GHCi from the sources directory, since it will find the module in it and try to use it instead of the installed modules in ~/.cabal folder.
You can also test the installation by writing a program:
import HSH.Command
main :: IO ()
main = do
runIO "date"
You can compile and execute the above as shown below:
$ ghc --make test.hs
[1 of 1] Compiling Main ( test.hs, test.o )
Linking test ...
$ ./test
Sun Jan 4 14:25:19 IST 2015
You are encouraged to read the Cabal guide at https://www.haskell.org/cabal/ for information on specific fields and their options.