Automated testing

HTTP tests check response status, headers, bodies, and state changes through the real WAI application. Types help compose handlers, but runtime exceptions, incorrect responses, and authorization mistakes still need testing.

The application and specs below are compiled in the repository’s cookbook example. You can run them there with cabal test spock-cookbook-example, or create a small standalone project as follows.

Project files

Create a directory named spock-testing-example with src, app, and test subdirectories. Put this in spock-testing-example.cabal:

cabal-version: 2.0
name: spock-testing-example
version: 0.1.0.0
build-type: Simple

library
  hs-source-dirs: src
  exposed-modules: Hello
  build-depends: base >= 4.12 && < 5, Spock >= 0.16 && < 0.17, wai
  default-language: Haskell2010
  ghc-options: -Wall

executable spock-testing-example
  hs-source-dirs: app
  main-is: Main.hs
  build-depends: base, Spock, spock-testing-example
  default-language: Haskell2010
  ghc-options: -Wall -threaded

test-suite http-tests
  type: exitcode-stdio-1.0
  hs-source-dirs: test
  main-is: Spec.hs
  other-modules: HelloSpec
  build-depends: base, Spock, spock-testing-example, hspec, hspec-wai
  default-language: Haskell2010
  ghc-options: -Wall -threaded

Use the same stack.yaml as Getting Started:

resolver: nightly-2026-09-01
compiler: ghc-9.14.1
packages:
  - .
extra-deps:
  - git: https://github.com/agrafix/Spock.git
    commit: 1d9bd1d6c89a2ad1bf590f53cc182d68e0ca8fd1
    subdirs:
      - Spock
      - Spock-core
      - reroute

This is a Cabal-only project. If adapting an Hpack project, put its dependencies in package.yaml instead of editing the generated Cabal file.

Separate configuration from routes

Create src/Hello.hs:

{-# LANGUAGE OverloadedStrings #-}

module Hello (app) where

import Network.Wai (Middleware)
import Web.Spock
import Web.Spock.Config

app :: IO Middleware
app = do
  cfg <- defaultSpockCfg () PCNoDatabase ()
  spock cfg routes

routes :: SpockM () () () ()
routes = do
  get root $ text "Hello World!"
  get ("hello" <//> var) $ \name -> text ("Hello " <> name)

app builds middleware; routes registers handlers. Passing routes to spock keeps setup separate from serving requests. Put the executable’s entry point in app/Main.hs:

module Main (main) where

import Hello (app)
import Web.Spock (runSpock)

main :: IO ()
main = runSpock 8080 app

Exercise the WAI application

Create test/HelloSpec.hs:

{-# LANGUAGE OverloadedStrings #-}

module HelloSpec (spec) where

import Hello (app)
import Test.Hspec
import Test.Hspec.Wai
import Web.Spock (spockAsApp)

spec :: Spec
spec = with (spockAsApp app) $ do
  describe "GET /" $
    it "serves the home page" $
      get "/" `shouldRespondWith` "Hello World!" { matchStatus = 200 }
  describe "GET /hello/:name" $ do
    it "greets the captured name" $
      get "/hello/Spock" `shouldRespondWith` "Hello Spock"
    it "returns 404 for an unmatched route" $
      get "/missing" `shouldRespondWith` 404

Then create test/Spec.hs:

module Main (main) where

import qualified HelloSpec
import Test.Hspec (hspec)

main :: IO ()
main = hspec HelloSpec.spec

spockAsApp app converts middleware to an IO Application. Test.Hspec.Wai.with builds an application for each example, and shouldRespondWith checks its response. Match the exact body when it matters: the home page here sends Hello World!, including the exclamation mark. Use matchHeaders to assert response headers and matchStatus for status codes.

stack test --fast --pedantic

These tests run in process without opening a network port. Commit the generated stack.yaml.lock to retain the resolved dependencies.

Test failures and separate clients

Add invalid input, missing resources, denied access, and repeated updates to your tests. The REST example tests use disposable SQLite databases and check persistence after closing and reopening the pool. The security example tests cover tokens, old session IDs, logout, and escaping.

WAI’s test client remembers cookies inside a session and prepends them to later requests. Omitting an explicit Cookie header therefore does not necessarily simulate another visitor. For tests involving distinct clients, use separate Network.Wai.Test.runSession calls against the same application and supply only the selected client’s cookies, or explicitly clear the client jar. The security example uses separate calls to verify that one visitor’s token cannot be used by another visitor.

See hspec-wai’s documentation for additional matchers and setup helpers, then continue with the request cookbook.