Roll Your Own Mock Framework
Level: Advanced 60–90 minConcepts: MockingDesign PatternsBoundariesIncremental Design
Solutions: C# | TypeScript | Python
Build a basic mock object framework that lets developers create test doubles, set up return values, and verify method calls. Understanding how mocks work under the hood makes you better at using them.
Inspired by Jason Gorman’s Codemanship kata.
Requirements
Step 1: Create a Mock
Given an interface or class, create a mock instance that implements the same contract.
interface Calculator {
add(a, b): number
subtract(a, b): number
}
mock = createMock(Calculator)
The mock should accept any method call without throwing.
Step 2: Stub Return Values
Configure a mock to return specific values when methods are called with specific arguments.
when(mock.add(2, 3)).thenReturn(5)
result = mock.add(2, 3) // returns 5
result = mock.add(1, 1) // returns undefined/null (no stub configured)
Step 3: Verify Invocations
After exercising the system under test, verify that expected methods were called.
mock.add(2, 3)
verify(mock.add).wasCalledWith(2, 3) // passes
verify(mock.add).wasCalledWith(1, 1) // fails with meaningful message
verify(mock.subtract).wasCalled() // fails — never called
Step 4: Meaningful Failure Messages
When verification fails, report what was expected vs what actually happened:
Expected: add(1, 1) to be called
Actual: add was called with (2, 3)
Test Cases
| Scenario | Expected |
|---|---|
| Create mock from interface | Mock instance created, methods callable |
| Call unstubbed method | Returns null/undefined, no error |
| Stub return value | Returns configured value for matching args |
| Stub with different args | Each arg set returns its own value |
| Verify called method | Passes |
| Verify uncalled method | Fails with message |
| Verify wrong arguments | Fails with expected vs actual message |
| Verify call count | wasCalledTimes(2) passes if called twice |
Bonus
- Support
wasCalledTimes(n)— verify exact call count - Support
wasNeverCalled()— verify a method was not invoked - Support argument matchers —
verify(mock.add).wasCalledWith(anyNumber(), 3) - Support
thenThrow(error)— mock throws instead of returning - Support call ordering — verify method A was called before method B
Hint
The core mechanic is a proxy that records all method calls (name + arguments) in a list. Stubbing adds entries to a return-value lookup. Verification searches the recorded calls. Start with recording calls and verifying them — add stubbing after.
Reference Walkthrough
Full C#, TypeScript, and Python implementations live at tddbuddy-reference-katas/roll-your-own-mock-framework with twelve scenarios across all three languages, a fluent when/verify API, and the domain exception VerificationError that distinguishes verification failures from unexpected errors. This is a meta-kata — the proxy mechanism diverges sharply per language (C# DynamicObject, TS Proxy with get trap, Python __getattr__).
- C# (.NET 8, xUnit, FluentAssertions) — walkthrough
- TypeScript (Node 20, Vitest, strict types) — walkthrough
- Python (3.11, pytest, dataclasses) — walkthrough
This kata ships in Agent Full-Bake mode (middle gear) — a single commit per language with the full domain design. See the repo’s Gears section for why that’s a deliberate teaching choice.
Related reading
- Test Deletion Is a Privileged Operation
The cheapest way for an agent to make a failing test pass is to delete it. That is logical for the agent and catastrophic for the codebase. Tests are append-only by default. Deletion needs a human author, a separate commit, and a separate review. - The Instruction File Is Not the Discipline
Teams that want their agent to do TDD reach for the instruction file. The TDAD paper measured this directly: adding a 'do TDD' instruction raised regressions by nearly two-thirds. Contextual test discovery cut them by seventy percent. The instruction is theatre. The codebase's test surface is the discipline. - Tamper-Resistant Test Design Is What the Suite Now Owes the Codebase
Tests were a contract the team made with itself. Agents made the suite an attack surface. Tamper-resistant test design is what the suite now owes the codebase: expensive to spoof, redundantly verified, partially hidden, and impossible to delete without a separate review.