liveBook · Manning

Test interpreter

As explained in Functional Design and Architecture: Examples in Haskell

A test interpreter is an interpreter implementation that evaluates scenario steps while recording or simulating effects for verification.

A test interpreter evaluates scenario steps without requiring every effect to be real. It consumes configurable mocks, records execution details, and can selectively delegate individual calls to a real interpreter before returning to test evaluation. section 15.2.4 section 15.2.3

Use the interpreter to control what a scenario evaluates and what it records:

  • Provide mock results for expected steps, accepting that an incorrectly typed mock may fail at test replay. section 15.2.4
  • Simulate failures, count operations, or collect a scenario call stack for verification. section 15.2.4
  • Mix white-box testing with integration behavior by marking selected steps RunRealInterpreter and the remainder RunTestInterpreter. section 15.2.3

Verifying a failed game load

A game-loading scenario supplies mocked database connections and results, then asserts that a failed player-position lookup raises the expected exception. A real-runtime variant marks selected calls for live evaluation while retaining test-controlled mocks elsewhere. With tracing enabled, the resulting stack records operations such as EvalLang, database loading, and exception throwing, making the failure path directly inspectable. section 15.2.3 section 15.2.4

Listing 15.16 Dispatching the interpreter
interpretAppF :: TestRuntime -> AppF a -> IO a
interpretAppF testRt fAct@(EvalLang action next) = do
  
  when (traceSteps testRt)                           #1
      $ modifyIORef (("EvalLang", ""):) 
      $ callStack testRt
  
  mbStep <- popNextStep testRt                       #2
  let mbRealAppRt = appRuntime testRt
  
  case (mbStep, mbRealAppRt) of                      #3
    (Just (Mock ghcAny), _) ->                       #4
       pure (next (unsafeCoerce ghcAny))
    (Just RunTestInterpreter, _) -> do               #5
       res <- runLangL testRt action
       pure (next res)
    (Just RunRealInterpreter, Just appRt) ->         #6
       Real.interpretAppF appRt fAct
    (Just RunRealInterpreter, Nothing) ->            #7
       error "Real runtime is not ready."
    (Nothing, _) -> error "Mock not found."

The dispatch listing makes the control flow concrete: pop the next Step, return a mocked value, run test evaluation, delegate one call to the real interpreter, or report a missing mock. section 15.2.4

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Explains step dispatch, mock handling, real-runtime delegation, call-stack tracing, and missing-mock errors through implementation listings.
Examples
Uses game-session loading, mocked key-value database failures, call-stack assertions, simulated subsystem failures, and counted scenario steps.
What it assumes
Assumes hierarchical free-monadic languages and configurable mocks while demonstrating test and real runtimes through concrete scenarios.
What you can do after
Diagnose/debug scenario behavior with mocks, recorded call stacks, controlled failures, operation counts, and selectively real effects.
How it connects
Connects free monads, configurable mocks, white-box testing, black-box integration testing, and real interpreters.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Design and Architecture: Examples in Haskell. The section links point to the parts of the book it draws on.