hspec: Testing framework

Hspec is a testing framework. It provides a friendly DSL for defining tests. It supports writing various kind of tests including unit, integration and property testing. tasty is another popular testing framework for Haskell.

Get started

$ stack new mylib rio --resolver lts-13.28
$ cd mylib
$ stack test
...
mylib> Test suite mylib-test passed

Woohoo, let's kick this off

Test driven development

Create an src/Reverse.hs:

{-# LANGUAGE NoImplicitPrelude #-}
module Reverse
  ( myReverse
  ) where
import RIO

myReverse :: [a] -> [a]
myReverse = undefined

Unit Testing

Create test/ReverseSpec.hs

module ReverseSpec where

import Test.Hspec
import Test.Hspec.QuickCheck
import Reverse

spec :: Spec
spec = do
  describe "myReverse" $ do
    it "handles empty lists" $ myReverse [] `shouldBe` ([] :: [Int])

And now run in a terminal:

$ stack test --file-watch --fast

It fails, of course:

Failures:
  src/Reverse.hs:10:13: 
  1) Reverse.myReverse handles empty lists
       uncaught exception: ErrorCall
       Prelude.undefined
       CallStack (from HasCallStack):
         error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
         undefined, called at src/Reverse.hs:10:13 in mylib-0.1.0.0-FCBjIqB4mhCEtNQa5Mn13e:Reverse

Fix it...

myReverse :: [a] -> [a]
myReverse [] = []

Tests pass. Add another test...

    it "reverses hello" $ myReverse "hello" `shouldBe` "olleh"

It breaks, add more code:

myReverse (x:xs) = myReverse xs ++ [x]

Property testing

Yay! Let's add a property:

prop "double reverse is id" $ \list ->
    myReverse (myReverse list) `shouldBe` (list :: [Int])

That's inefficient, let's create a better reverse function. Again, TDD:

-- code
betterReverse :: [a] -> [a]
betterReverse = undefined

-- test
describe "betterReverse" $ do
  prop "behaves the same as myReverse" $ \list ->
    betterReverse list `shouldBe` myReverse (list :: [Int])

Notice Remember to export the betterReverse function at top of the code file.

Exercise Why is myReverse slow, and how can you make it faster?

Real buggy code I wrote by mistake...

betterReverse :: [a] -> [a]
betterReverse =
    loop []
  where
    loop res [] = []
    loop res (x:xs) = loop (x:res) xs

Test suite catches me!

  test/ReverseSpec.hs:16:7: 
  1) Reverse.betterReverse behaves the same as myReverse
       Falsifiable (after 4 tests and 3 shrinks):
         [0]
       expected: [0]
        but got: []

OK, let's fix the code:

betterReverse :: [a] -> [a]
betterReverse =
    loop []
  where
    loop res [] = res
    loop res (x:xs) = loop (x:res) xs

Hurrah!

Let's play with vector

First let's add a vector dependency to package.yaml:

- vector >= 0.12.0.3

New Reverse.hs will now look like that:

{-# LANGUAGE NoImplicitPrelude #-}
module Reverse
  ( myReverse
  , betterReverse
  , vectorReverse
  , uvectorReverse
  , svectorReverse
  ) where

import RIO
import qualified RIO.Vector as V
import qualified RIO.Vector.Boxed as VB
import qualified RIO.Vector.Storable as VS
import qualified RIO.Vector.Unboxed as VU
import qualified Data.Vector.Generic.Mutable as VM

myReverse :: [a] -> [a]
myReverse [] = []
myReverse (x:xs) = myReverse xs ++ [x]

betterReverse :: [a] -> [a]
betterReverse =
    loop []
  where
    loop res [] = res
    loop res (x:xs) = loop (x:res) xs

vectorReverseGeneric
  :: V.Vector v a
  => [a]
  -> v a
vectorReverseGeneric input = V.create $ do
  let len = length input
  v <- VM.new len
  let loop [] idx = assert (idx == -1) (return v)
      loop (x:xs) idx = do
        VM.unsafeWrite v idx x
        loop xs (idx - 1)
  loop input (len - 1)
{-# INLINEABLE vectorReverseGeneric #-}

vectorReverse :: [a] -> [a]
vectorReverse = VB.toList . vectorReverseGeneric
{-# INLINE vectorReverse #-}

svectorReverse :: VS.Storable a => [a] -> [a]
svectorReverse = VS.toList . vectorReverseGeneric
{-# INLINE svectorReverse #-}

uvectorReverse :: VU.Unbox a => [a] -> [a]
uvectorReverse = VU.toList . vectorReverseGeneric
{-# INLINE uvectorReverse #-}

Awesome, but are they working as advertised ? Let's find out!

Add few properties which compares the result with the implemention of reverse function in the standard libray:

describe "compare with Data.List reverse" $ do
  prop "vectorReverse" $ \list ->
    vectorReverse list `shouldBe` (reverse (list :: [Int]))
  prop "svectorReverse" $ \list ->
    svectorReverse list `shouldBe` (reverse (list :: [Int]))
  prop "uvectorReverse" $ \list ->
    uvectorReverse list `shouldBe` (reverse (list :: [Int]))

And yeah, all the tests passes fine!

  compare with Data.List reverse
    vectorReverse
      +++ OK, passed 100 tests.
    svectorReverse
      +++ OK, passed 100 tests.
    uvectorReverse
      +++ OK, passed 100 tests.