Mocking replaces a real dependency with a configurable test implementation. It lets tests run without physical hardware, native functions, or other unpredictable external subsystems, while deliberately producing values, failures, and edge cases that expose weaknesses in business logic. section 4.2.4 section 15.1
The enabling design choice is an interface boundary. Business logic calls a service handle, free-monad language, or other interface rather than a concrete implementation; a test then supplies a mock with the same operations. This preserves the application logic while changing the environment in which it runs. section 4.3 section 15.2.1
A useful mock should make the test's controlled conditions explicit:
- Return fake domain values for expected calls, including absent or invalid results that real systems may produce. section 4.2.4
- Provide configured values that steer evaluation into a particular branch of business logic. section 15.2.2
- Vary values across calls when a scenario needs a sequence rather than one fixed response. section 15.2.2
- Optionally reject impossible configured values so that an invalid test setup fails clearly. section 15.2.2
- Record calls when interaction itself matters, using mutable state only inside the test double. section 4.2.4
Mocking a hardware service
A device-assembly test substitutes both device creation and part lookup. The fake service builds a device from a two-component template, returns fake thermometer and pressure parts for known indexes, and returns Nothing for a requested third part. The test verifies that missing data is handled rather than causing a crash, without connecting any physical device. section 4.2.4
Listing 4.12 A test that uses a mocked hardware service
it "Getting absent device part" $ do
let testDef =
[ ComponentDef "t1" thermometer1Passp
, ComponentDef "p1" pressure1Passp
]
device <- makeDevice mockedHardwareService testDef #1
mpPart1 <- getDevicePart mockedHardwareService "t1" device #2
mpPart2 <- getDevicePart mockedHardwareService "p1" device #2
mpPart3 <- getDevicePart mockedHardwareService "t2" device #3
case (mpPart1, mpPart2, mpPart3) of #4
(Just _, Just _, Nothing) -> pure ()
_ -> fail "Device is assembled incorrectly." #5
This test shows dependency injection at each service call and checks known parts alongside an intentionally absent one. section 4.2.4
Free monads support the same separation through interpreters. A live random interpreter invokes random generation; a test interpreter can return a fixed value, validate it against the requested range, or consume a mutable sequence. This makes a multi-step scenario repeatable: configure 3 and then 2 to force the game-selection branch and choose Dwarf Fortress. section 15.2.2
Figure 15.3 Free monads, mocks, and white-box tests
The diagram situates mock interpreters between a free-monad interface and white-box tests, alongside real interpreters. section 15.2.2
Mocks are not a substitute for architecture. They depend on low coupling and interfaces that cannot be bypassed by business logic. Bare effects inside business logic undermine this arrangement because hidden side effects remain outside the mockable boundary. When dependencies are isolated, mocks support white-box unit tests; pure functions often need no mocking because their behavior is deterministic. section 15.1 section 15.2.1 section 15.3




















