We discuss here some basic ideas around testing, that you will further work on in the lab and in future labs and projects.
When we refer to tests, we typically mean automated tests.
Automated tests can be run automatically without requiring user interaction, and typically produce output that can be automatically consumed by other applications for the generation of reports and test summaries.
Those tests fall into various categories:
Unit tests test a single tiny individual component of your application. You typically want to have unit tests for every bit of your code that is part of your application’s interface.
You should avoid testing for things that are too implementation-dependent.
Unit tests are a crucial part of the refactoring process, whose goal is to rearrange and rewrite sections of your code. A solid suite of unit tests can allow you to do this freely without worrying about breaking code. “Your tests will catch that”. And version control allows you to recover if you’ve messed things up too much.
Timing tests are used in algorithm implementations to assess the efficiency of the algorithms.
They can also be used to try to find bottlenecks in your application, though some of the browser profiling tools might be better.
We will focus on unit tests for now.
In Test-Driven Development, you typically would follow these steps:
This is some of the general theory behind testing and test-driven development. We will now look at testing in Haskell.
Haskell offers a number of different testing systems that you can use. In this lab we will focus on two:
ghci
interpreter, or as part of a testing framework like HSpec.
We start by taking a look at QuickCheck. QuickCheck operates by testing “properties”. A property is simply a function that returns a boolean (we have been calling these “predicates”). In order for QuickCheck to do its work, it needs to be able to generate various inputs. QuickCheck already knows about many standard inputs, and you can provide it with “generators” for your custom input types.
Let us take a look at an example in the interpreter. We start by loading the QuickCheck module into our module:
import Test.QuickCheck
In order to use QuickCheck we must define “properties”. These are meant to be functions that should be true about any input you give them. QuickCheck can’t typically check all inputs, but it will check at random 100 inputs.
-- This is of course a "wrong" property, as it is not always true.
prop_allNumbersAreLessThan10 :: Int -> Bool
prop_allNumbersAreLessThan10 x = x < 10
It is customary to start the name of these properties via prop
.
We can now load our module in gchi
and test this property:
quickCheck prop_allNumbersAreLessThan10
You get back an answer that looks like this:
*** Failed! Falsifiable (after 15 tests and 1 shrink):
10
So QuickCheck will tell you that the test failed after trying 15 input values. You can try verboseCheck
to see the actual values tried:
verboseCheck prop_allNumbersAreLessThan10
Note that these tests are not perfect: They only test a small number of inputs. For example if we were using 100 instead of 10, we would have gotten back the answer that everything is OK! The test never checked numbers bigger than 100.
We can help QuickCheck along by providing “custom generators” for it. Generators have type Gen a
. There are a number of built-in functions that allow us to create new generators:
-- Random element between the two given values (inclusive)
choose :: Random a => (a, a) -> Gen a
-- Randomly chooses one of the generators from the list
oneOf :: [Gen a] -> Gen a
-- Randomly chooses a generator from the list, using the "weights" to determine frequency
frequency :: [(Int, Gen a)] -> Gen a
-- Randomly chooses an element from the list
elements :: [a] -> Gen a
-- Generates a value that passes the predicate
suchThat :: Gen a -> (a -> Bool) -> Gen a
-- Generates a value if the function returns a Just v
suchThatMap :: Gen a -> (a -> Maybe b) -> Gen b
-- Generates a list of elements of random length
listOf :: Gen a -> Gen [a]
listOf1 :: Gen a -> Gen [a] -- Guaranteed non-empty
vectorOf :: Int -> Gen a -> Gen [a] -- Specified length
shuffle :: [a] -> Gen [a] -- Random permutation of the list
-- Arbitrary t means that quickCheck knows how to generate values of type t
arbitrary :: Arbitrary a => Gen a
vector :: Arbitrary a => Int -> Gen [a]
orderedList :: (Ord a, Arbitrary a) => Gen [a]
You can use a specific generator as follows:
genSmallNumbers :: Gen Int
genSmallNumbers = choose (1, 9)
-- Note the different type
prop_allNumbersAreLessThan10 :: Property
prop_allNumbersAreLessThan10 = forAll genSmallNumbers $ \x -> x < 10
In this instance we tell it to use our genSmallNumbers
generator to produce the x
. That generator in turn only produces numbers between 1 and 9. Therefore QuickCheck will actually succeed in this instance.
Lets try to write a generator that returns powers of a certain base (one easy way to get somewhat large numbers). We will use the do
syntax for this, which you are probably not too familiar with, and we will discuss it more later.
genPowers :: Int -> Gen Int
genPowers base = do
expo <- choose (1, 40)
return (base ^ (expo :: Int))
prop_allNumbersAreLessThan100 :: Property
prop_allNumbersAreLessThan100 = forAll (genPowers 2) $ \x -> x < 100
Calling quickCheck
on prop_allNumbersAreLessThan100
will now actually fail (as it should) after just a few tries.
There are many more features that QuickCheck provides. You’ll want to look at its manual for more.
HSpec is used in order to write more systematic tests, using QuickCheck or direct cases along the way, and produce suitable input. With HSpec you would be creating a separate test file with a suitable main
method. We will discuss this kind of testing setup later, after we discuss IO in more detail.