Unit Testing with HUnit and Hspec
Pure functions are the easiest code in the world to test: same input, same output, no setup. Haskell's testing ecosystem has three main libraries, often used together:
- HUnit — classic unit tests with assertions
- Hspec — a behavior-driven framework (
describe/it) that runs HUnit-style and QuickCheck tests side by side - QuickCheck — property-based testing (next lesson)
Add them to your test suite's build-depends in the .cabal file
(hspec, HUnit, QuickCheck) and cabal downloads them from Hackage.
HUnit
import Test.HUnit
import System.IO (stdout)
slugify :: String -> String
slugify = map (\c -> if c == ' ' then '-' else c)
tests :: Test
tests =
TestList
[ "spaces become dashes" ~: slugify "hello world" ~?= "hello-world"
, "empty string" ~: slugify "" ~?= ""
, "lowercases" ~: slugify "Hello" ~?= "hello"
]
main :: IO ()
main = do
(counts, _) <- runTestText (putTextToHandle stdout False) tests
print counts
### Failure in: 2:lowercases
main.hs:12
expected: "hello"
but got: "Hello"
Cases: 3 Tried: 3 Errors: 0 Failures: 1
Counts {cases = 3, tried = 3, errors = 0, failures = 1}
label ~: testnames a test;actual ~?= expectedasserts equality.- The third test fails on purpose, and the report shows the expected and
actual values. (
runTestTT testsis the shorter way to run them; it also prints progress to the terminal.) - In a cabal test suite, exit with a failure code when tests fail, so
cabal testand CI notice — Hspec does this for you.
Hspec
Hspec reads like a specification:
-- illustrative: test/Spec.hs, run with cabal test
import Test.Hspec
import Control.Exception (evaluate)
slugify :: String -> String
slugify = map (\c -> if c == ' ' then '-' else c)
main :: IO ()
main = hspec $ do
describe "slugify" $ do
it "replaces spaces with dashes" $
slugify "hello world" `shouldBe` "hello-world"
it "leaves an empty string alone" $
slugify "" `shouldBe` ""
describe "div" $ do
it "throws on division by zero" $
evaluate (1 `div` (0 :: Int)) `shouldThrow` anyArithException
describe "division" $ do
it "is exact for multiples" $
(12 `div` 4) `shouldSatisfy` (== 3)
slugify
replaces spaces with dashes [✔]
leaves an empty string alone [✔]
div
throws on division by zero [✔]
division
is exact for multiples [✔]
Finished in 0.0008 seconds
4 examples, 0 failures
shouldBe,shouldSatisfy,shouldReturn(for IO actions), andshouldThrowcover most assertions.hspec-discovercan find every*Spec.hsfile automatically, so large projects put one spec module next to each source module.- Run a subset with
cabal test --test-options='--match "slugify"'.
What to test
Keep IO at the edges (Level 2) and most of your code is pure — test it
directly. For code that does IO, pass the effectful parts in as
arguments or use a type class (see the mtl lesson), so tests can supply
fakes. The doctest tool also runs the >>> examples in Haddock
comments as tests, which keeps documentation honest.