100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Testing Haskell Code

Learn how to write unit tests, property-based tests, and doctest examples for Haskell code using HUnit, Hspec, and QuickCheck.

Practical HaskellIntermediate10 min readJul 10, 2026
Analogies

Unit Testing with HUnit and Hspec

Hspec is the most widely used BDD-style testing framework in Haskell, built on top of HUnit-style assertions. It lets you write nested describe/it blocks with matchers like shouldBe and shouldSatisfy to state exactly what a function should return for a given input. Test-suites are declared in the .cabal file with type: exitcode-stdio-1.0 and a main-is entry point, and are run via cabal test or stack test, which compile the test-suite into a standalone executable.

🏏

Cricket analogy: Writing describe "reverse" $ it "returns [] for []" $ reverse [] shouldBe [] is like a coach running a specific fielding drill and checking one exact outcome -- catching a high ball cleanly -- rather than vaguely hoping the fielder 'does well' in a real match.

Property-Based Testing with QuickCheck

QuickCheck lets you state a property like prop_reverseTwice xs = reverse (reverse xs) == xs and it generates hundreds of random inputs, via the Arbitrary typeclass, trying to falsify that property. When it finds a failing case, QuickCheck automatically shrinks the input, trying smaller and simpler variants, down to the smallest counterexample that still fails, which makes debugging dramatically easier than staring at one huge random failing input.

🏏

Cricket analogy: QuickCheck generating hundreds of random input lists to try to break prop_reverseTwice is like a bowling machine firing hundreds of deliveries at varying pace and length to stress-test a batter's technique, rather than bowling the same three balls a coach already knows they can handle.

haskell
-- test/Spec.hs
import Test.Hspec
import Test.QuickCheck
import Data.List (sort)

reverseTwice :: [a] -> [a]
reverseTwice = reverse . reverse

main :: IO ()
main = hspec $ do
  describe "reverseTwice" $ do
    it "returns [] for an empty list" $
      reverseTwice ([] :: [Int]) `shouldBe` []

    it "is the identity function" $
      property $ \xs -> reverseTwice (xs :: [Int]) == xs

  describe "sort" $
    it "produces a non-decreasing list" $
      property $ \xs ->
        let ys = sort (xs :: [Int])
        in and (zipWith (<=) ys (drop 1 ys))

Golden Tests and Doctest

Golden tests, provided by libraries like tasty-golden, compare a function's output against a saved reference ('golden') file and flag any unexpected diff -- ideal for complex or generated output such as rendered HTML, pretty-printed ASTs, or serialized JSON that would be tedious to assert field-by-field. Doctest takes a different approach: it scans Haddock documentation comments for >>> example blocks and actually runs them as executable tests, so a usage example in your documentation can never silently drift out of sync with the real implementation.

🏏

Cricket analogy: A golden test comparing a function's output byte-for-byte against a saved reference file is like comparing today's scorecard format against last season's officially approved template -- any unexpected deviation, even a formatting change, gets flagged for review.

QuickCheck's default Arbitrary instance for lists or Int may skew toward small or 'nice' values and rarely generate edge cases like empty lists, negative numbers, or Unicode strings unless you write a custom Arbitrary instance with a tailored shrink -- always check the generated distribution with sample before trusting a green property test.

Structuring Test Suites with Tasty

Tasty is a unifying test framework that combines HUnit-style assertions, Hspec-style specs, QuickCheck properties, and golden tests into a single runnable tree, using defaultMain (testGroup "All Tests" [...]) as the entry point. This lets a project have one consistent test runner, one consistent CLI (with options like --pattern to filter which tests run and --quickcheck-tests to adjust how many random cases QuickCheck generates), and one consolidated pass/fail report instead of juggling several disconnected test binaries.

🏏

Cricket analogy: Tasty combining HUnit, QuickCheck, and golden tests into a single runnable testGroup tree is like an ICC tournament combining Test, ODI, and T20 results under one unified points system, letting selectors see a player's overall form across formats in one report.

In CI, run cabal test --test-show-details=streaming (or stack test) so test output streams live instead of being buffered until the whole suite finishes -- this makes it much faster to spot which specific test is hanging or failing in a long pipeline log.

  • Hspec provides a BDD-style describe/it syntax on top of HUnit-style assertions like shouldBe for exact-value unit tests.
  • QuickCheck tests properties (prop_...) against hundreds of randomly generated inputs via the Arbitrary typeclass, and shrinks failures to a minimal counterexample.
  • Golden tests compare a function's output against a saved reference file, flagging any unexpected diff, and are ideal for complex or rendered output.
  • Doctest runs example usages embedded directly in Haddock comments as executable tests, keeping documentation honest.
  • Custom Arbitrary instances with tailored generators/shrinkers are often needed since default generators can under-sample edge cases.
  • Tasty unifies HUnit, Hspec, QuickCheck, and golden tests into one runnable testGroup tree via defaultMain.
  • Test-suites are declared in the .cabal file (type: exitcode-stdio-1.0) and run with cabal test or stack test.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#TestingHaskellCode#Testing#Haskell#Code#Unit#StudyNotes#SkillVeris#ExamPrep