Haskell Project: Unit Testing with Hspec

:: haskell, tutorial, hspec, testing

Up to this point we have written some code, explained type classes, and done a little pattern matching. But we haven’t written anything that really does anything. Until we have some IO to perform, we will have to find another way to interact with our code. This is a perfect chance to introduce Unit Testing.

If you want to review what we’ve done so far, here is the previous post Show, Compare, and Filter. Next post Intro to Parsers.

Broken Code in the repl

Starting where we left off in the previous post. We created a Date type that was an instance of type class Show.

-- |Show Date in YYYY-MM-DD format
instance Show Date where
  show (Date year month day) = show year
                               ++ "-" ++ show month
                               ++ "-" ++ show day

When we printed the Date type in our repl things looked right…as long as we had double digit months and days. We wanted to always get four digit years, two digit months and days.

>>> let x = (Date 2017 10 20)
>>> x
2017-10-20
>>> let y = (Date 2017 1 9)
>>> y
2017-1-9

Our code is broken, and we only detected it by playing around in the repl. Not a good way to do development.

Adding Unit Testing with Hspec

There are tons of Unit Testing suites out there. All with their own reasons as to why they are exactly what you need. I went with Hspec because I’ve used Ruby’s RSpec in the past. The Hspec site lists the following features:

  • A friendly DSL for defining tests
  • Integration with QuickCheck, SmallCheck and HUnit
  • Parallel test execution
  • Automatic discovery of test files

So it looks like Hspec supports other major unit testing suites which is great in case you need to change for some reason. Friendly DSL and automatic detection are good too.

So lets add Hspec into our Stack build. Editing todo.cabal, under the test-suite section we need to add a build dependency for hspec.

test-suite todo-test
  type:               exitcode-stdio-1.0
  hs-source-dirs:     test
  main-is:            Spec.hs
  build-depends:      base
                    , todo
                    , hspec
  ghc-options:        -Wall
  default-language:   Haskell2010

A quick overview of this section. hs-source-dirs is the location in the project for tests, and main-is defines the main source file that will be executed when running tests. build-depends defines all the packages required to build the test binary.

To add in the automatic discovery we need to make a simple modification. Replace test/Spec.hs with the following. Yes, the file should contain just this single line.

{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

Now we can just add files to the test directory and they will automatically be included in the test. The naming structure is pretty easy:

src/<PackageName>.hs -> test/<PackageName>Spec.hs

Our first test

Now that we have everything configured correctly, and our tests are auto-discovered we can add in our first test: test/TasksSpec.hs

module TasksSpec where

import Tasks

import Test.Hspec (Spec, describe, context, it, shouldBe)

-- |Required for auto-discpvery
spec :: Spec
spec =
  describe "Task Data Types" $ do
    describe "Date" $ do

      it "Shows YYYY-MM-DD with single digit month and day" $ do
        show (Date 2017 2 1) `shouldBe` "2017-02-01"

      it "Shows YYYY-MM-DD with double digit month and day" $ do
        show (Date 2017 12 11) `shouldBe` "2017-12-11"

The format should be pretty simple to understand and if it doesn’t yet, seeing the output might make more sense.

If you’d like to get a full understanding of how this all works check out the Hspec Documentation over at Hackage. But the simple way of looking at it you have a description of what you’re going go test, and then list what it should do.

When we run stack test in the command line we will see Stack download, configure and build all the dependencies for Hspec. Once you’ve done this all new projects will use the local copy so this process will speed up.

$ stack test
...
<a bunch of download/configure/build/copy/register>
...
Tasks
  Task Data Types
    Date
      Shows YYYY-MM-DD with single digit month and day FAILED [1]
      Shows YYYY-MM-DD with double digit month and day

Failures:

  test\TasksSpec.hs:12:
  1) Tasks, Task Data Types, Date, Shows YYYY-MM-DD with single digit month and day
       expected: "2017-02-01"
        but got: "2017-2-1"

Randomized with seed 1995209293

Finished in 0.0060 seconds
2 examples, 1 failure

Completed 17 action(s)
Test suite failure for package todohs-example-0.0.0.1
    todohs-example-test:  exited with: ExitFailure 1
Logs printed to console

Looking at the output we can see the tree of our test spec.

<Module>Spec -> Tasks
description  ->   Task Data Types
description  ->     Date
it "should"  ->       Shows YYYY-MM-DD...

We see that one of our tests passed (double digits) while the other test failed.

Fix and Retest

From our failure results we can compare our expected to our actual results.

  test\TasksSpec.hs:12:
  1) Tasks, Task Data Types, Date, Shows YYYY-MM-DD with single digit month and day
       expected: "2017-02-01"
        but got: "2017-2-1"

We need to pad single digit numbers with zero. Should be simple by adding in a helper function.

-- |Show Date in YYYY-MM-DD format
instance Show Date where
  show (Date year month day) = show year
                               ++ "-" ++ showDoubleDigit month
                               ++ "-" ++ showDoubleDigit day
    where showDoubleDigit num = if num < 10
                                then "0" ++ show num
                                else show num

Since we have our tests in place we can just run them again and see if this fixes our issue.

$ stack test
...
<building>
...
Tasks
  Task Data Types
    Date
      Shows YYYY-MM-DD with single digit month and day
      Shows YYYY-MM-DD with double digit month and day

Finished in 0.0020 seconds
2 examples, 0 failures

Completed 2 action(s).

We pass both tests!

Next step is to implement all the tests required for the other type classes we implemented for the other data types. See the source code for a large example of what that might look like.

Red/Green/Refactor

I thought it might be useful to state that once you have your unit test suite in place, I like to continue development doing a Test-driven Development technique called “Red/Green/Refactor”. Its pretty easy to understand and follow:

  • Red: Create a test first before working code. It will fail (with red colored output)
  • Green: Add the required support so test passes (with green colored output)
  • Refactor: Code is working and tests pass, refactor while always passing test

There is nothing saying you have to follow this methodology but I find it to be useful.

Up Next

A diff of all changes in this post are available here.

In the next post we will get a little more complicated and start working with parsers for both text files and user input.