When we write Haskell, we rely on the compiler to judge for us whether our code is well formed.
The compiler prevents many errors, but it doesn't prevent them all.
It is still possible to get runtime errors even if there aren't any syntax errors.
Tests allow you to state an expectation and then verify that the result of an operation meets that expectation.
For the sake of simplicity, there are two broad categories of testing: unit testing and property testing.
Unit testing allows you to check that each function is performing the task it is meant to do.
Property testing tests the formal properties of programs without requiring formal proofs by allowing you to express a truth-valued, universally quantified function, which will then be checked against randomly generated inputs.
We will use a library called hspec to demonstrate the means of writing tests.
Let's create a new project for experimentation called "addition" using cabal:
$ cabal init -n
Guessing dependencies...
Generating LICENSE...
Warning: unknown license type, you must put a copy in LICENSE yourself.
Generating Setup.hs...
Generating CHANGELOG.md...
Generating Main.hs...
Generating Addition.cabal...
Next, specify the hspec dependency:
-- addition.cabal
name: addition
version: 0.1.0.0
license-file: LICENSE
author: Clayton Davidson
maintainer: clayton.davidson847@topper.wku.edu
category: Text
build-type: Simple
cabal-version: >=1.10
library
exposed-modules: Addition
ghc-options: -Wall -fwarn-tabs
build-depends: base >=4.7 && <5
, hspec
hs-source-dirs: .
default-language: Haskell2010
Create a file called "Addition.hs":
-- Addition.hs
module Addition where
main :: IO ()
main = putStrLn "placeholder"
Create an empty LICENSE so the build doesn't complain:
$ touch LICENSE
Now you should have a directory like this:
$ tree
.
├── addition.cabal
├── Addition.hs
└── LICENSE
0 directories, 3 files
Next, initialize the Stack file for describing the snapshot of Stackage
$ stack init
Then, build the project:
$ stack build
Fire up the REPL:
$ stack ghci
[1 of 1] Compiling Addition ( Addition.hs, interpreted )
Ok, one module loaded.
Prelude> main
placeholde
Let's now import hspec
's primary module:
-- Addition.hs
module Addition where
import Test.Hspec
main :: IO ()
main = putStrLn "placeholder"
We can now create our first test:
-- Addition.hs
module Addition where
import Test.Hspec
main :: IO ()
main = hspec $ do
describe "Addition" $ do
it "1 + 1 is greater than 1" $ do
(1 + 1) > 1 `shouldBe` True
Here, we assert that (1 + 1) should be greater than 1, and that is what hspec
will test for us.
On run we get:
Prelude> main
Addition
1 + 1 is greater than 1
Finished in 0.0002 seconds
1 example, 0 failures
What happens here? Basically hspec
runs your code and verifies that the arguments you passed to shouldBe
are equal.
Let's run another set of tests:
-- Addition.hs
module Addition where
import Test.Hspec
main :: IO ()
main = hspec $ do
describe "Addition" $ do
it "1 + 1 is greater than 1" $ do
(1 + 1) > 1 `shouldBe` True
it "2 + 2 should be equal to 4" $ do
(2 + 2) `shouldBe` 4
Prelude> main
Addition
1 + 1 is greater than 1
2 + 2 should be equal to 4
Finished in 0.0007 seconds
2 examples, 0 failures
hspec
is a great library and all, but we can do better.
hspec
only allows us to prove something about particular values.
Can we get assurances that are stronger, maybe something closer to proofs?
QuickCheck
allows us to utilize property testing.
Property testing is done with the assertion of laws or properties.
Add QuickCheck
to the .cabal
file like you did with hspecs
.
Once that is complete, add the following to your module:
import Test.QuickCheck
-- other tests
it "x + 1 is always greater than x" $ do
property $ \x -> x + 1 > (x :: Int)
Assuming all is well, when you run it, you will see something like this:
Addition
1 + 1 is greater than 1
2 + 2 should be equal to 4
x + 1 is always greater than x
+++ OK, passed 100 tests.
Finished in 0.0070 seconds
3 examples, 0 failures
As you can see, QuickCheck
tests many values to see if the assertions hold for all of them.
It does this by randomly generating values of the type you said you expected.
The number of tests QuickCheck
runs defaults to 100.
Arbitrary Instances
QuickCheck
relies on a typeclass called Arbitrary
and a newtype
called Gen
for generating its random data.
We can use sample
and sample
from the Test.QuickCheck
module to see random data:
-- prints each value on a new line
Prelude> :t sample
sample :: Show a => Gen a -> IO ()
-- returns a list
Prelude> :t sample'
sample' :: Gen a -> IO [a]
Hspec - https://hspec.github.io/
QuickCheck - https://hackage.haskell.org/package/QuickCheck
Haskell Book - https://haskellbook.com/