Haskell Testing
Default test stack: hspec for structure + QuickCheck for property tests, with hspec-discover for auto-discovery. For doctest setup (executable examples in Haddock comments), see haskell-documentation.
Test-suite cabal stanza
test-suite myapp-test
import: shared
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Spec.hs
build-depends:
, base
, myapp
, hspec ^>=2.11
, QuickCheck ^>=2.14
build-tool-depends: hspec-discover:hspec-discover
The test/Spec.hs file is a one-liner that turns on hspec-discover:
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
That's it — hspec-discover walks test/ looking for *Spec.hs modules whose path mirrors the source module structure, and assembles them into one test executable.
Test file conventions
Tests are auto-discovered when they match <Module>Spec.hs and export spec :: Spec:
module User.RegistrationSpec (spec) where
import Test.Hspec
import Test.QuickCheck
import User.Registration
spec :: Spec
spec = describe "registerUser" $ do
it "rejects empty emails" $
parseEmail "" `shouldBe` Left EmptyEmail
prop "round-trips through encode/decode" $ \(email :: Email) ->
parseEmail (renderEmail email) === Right email
Conventions:
- One spec module per source module, mirroring the directory structure.
src/User/Registration.hs↔test/User/RegistrationSpec.hs. - Property tests for laws and round-trips. Anything you'd state as "for all X, ..." belongs in
prop. Unit tests (it) for specific edge cases QuickCheck won't reliably find. Arbitraryinstances for domain types — define them in aTest.Arbitrarymodule (or similar test-only module), not in the main library, so production code doesn't depend on QuickCheck.- No
IOin tests when avoidable. Use the in-memory interpreters from youreffectfulsetup (seehaskell-effectful). Pure logic should be tested purely.
Detailed, colored test output
Two pieces of configuration to make cabal test show each it as it runs, with green/red coloring.
In cabal.project add test-show-details: direct — see haskell-project-setup for the full file.
In .hspec (project root) — configures the hspec output format:
--format=specdoc
--color
specdoc is the nested describe/it tree format. --color forces ANSI colors on (hspec auto-detects terminals but it's worth being explicit). Both files should be committed so the team shares the same test UX.
For CI environments, terminals are often not detected as interactive and colors get disabled automatically. Force them on via env var or explicit flag:
# GitHub Actions, GitLab CI, etc.
env:
HSPEC_OPTIONS: --color
Or one-off:
cabal test --test-options="--color"
The resulting output looks roughly like:
User.Registration
parseEmail
rejects empty input [✔]
accepts valid format [✔]
registerUser
fails when email already exists [✔]
rejects passwords shorter than 8 chars [✘]
Failures:
test/User/RegistrationSpec.hs:42:
1) registerUser rejects passwords shorter than 8 chars
expected: Left (PasswordTooShort 4 8)
but got: Right (User ...)
7 examples, 1 failure
Testing effectful code
When the system under test uses effectful, write an in-memory interpreter for each effect and assemble the same kind of runEff . ... $ action chain you'd use in main, just with test-shaped interpreters:
spec :: Spec
spec = describe "registerUser" $ do
it "saves a new user" $ do
let (result, store) = runPureEff
. runState Map.empty
. runErrorNoCallStack @RegistrationError
. runUserStoreInMemory
$ registerUser someEmail
result `shouldSatisfy` isRight
Map.size store `shouldBe` 1
This is the payoff of the effectful design: tests run pure, fast, deterministic — no Docker, no fixtures, no mocks of database libraries.
Related
- Doctest setup (verifying
>>>examples in Haddock) is inhaskell-documentation— it shares the test-running infrastructure but is conceptually about docs. - The in-memory interpreter pattern:
haskell-effectful. - Cabal test-suite stanza in context of the full
.cabalfile:haskell-project-setup.