Learning Haskell: Testing

posted 2 years ago

A Quick Tour of Testing

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.

Conventional Testing

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

Enter Quickcheck

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]

Sources

Hspec - https://hspec.github.io/

QuickCheck - https://hackage.haskell.org/package/QuickCheck

Haskell Book - https://haskellbook.com/