# Haskell Testing

> Set up and write Haskell tests with `hspec`, `QuickCheck`, and `hspec-discover`. Covers the test-suite cabal stanza, auto-discovery, per-module spec layout, property tests, `Arbitrary` instance placement, in-memory effect interpreters, and colored/specdoc output (locally and in CI). Use when adding tests, configuring the test runner, deciding between unit and property tests, or making test output more readable.

- Skill: `ivelten/haskell-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ivelten/haskell-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ivelten/haskell-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: ivelten (https://skillmd.com/u/ivelten)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ivelten/haskell-testing

---


# 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

```cabal
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:

```haskell
{-# 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`:

```haskell
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.
- **`Arbitrary` instances for domain types** — define them in a `Test.Arbitrary` module (or similar test-only module), **not in the main library**, so production code doesn't depend on QuickCheck.
- **No `IO` in tests when avoidable.** Use the in-memory interpreters from your `effectful` setup (see `haskell-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:

```yaml
# GitHub Actions, GitLab CI, etc.
env:
  HSPEC_OPTIONS: --color
```

Or one-off:

```bash
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:

```haskell
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 in `haskell-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 `.cabal` file: `haskell-project-setup`.

