MP-301b · Module 2
Fixture Servers & Deterministic Testing
3 min read
A fixture server is a fully functional MCP server with all external dependencies replaced by deterministic in-memory implementations. It responds to the same tool calls as your production server but always returns the same results for the same inputs, regardless of time, network state, or database contents. Fixture servers are the foundation of reproducible testing — every test run produces identical results, making failures diagnosable and CI pipelines reliable.
The key design decision for fixture servers is data scope. Too little data and you cannot test realistic workflows. Too much data and the fixture is hard to maintain and reason about. The sweet spot is 10-20 records per entity type, covering all the states your tools care about: active, inactive, archived, edge-case-formatted, and missing. Name your fixture records descriptively — "acme-active," "globex-churned," "unicorp-archived" — so test assertions read like documentation.
// Fixture data covering all states your tools care about
export const customers = [
{ id: "CUS-001", name: "Acme Corp", status: "active", industry: "tech" },
{ id: "CUS-002", name: "Globex Inc", status: "churned", industry: "finance" },
{ id: "CUS-003", name: "Unicorp Ltd", status: "archived", industry: "healthcare" },
{ id: "CUS-004", name: "Edge\u00a0Case LLC", status: "active", industry: "tech" }, // non-breaking space
{ id: "CUS-005", name: "", status: "active", industry: "" }, // empty strings
] as const;
// In-memory implementation matching your real interface
export function createFixtureCrm() {
const data = new Map(customers.map(c => [c.id, { ...c }]));
return {
findCustomer: async (id: string) => data.get(id) ?? null,
listCustomers: async (filter?: { status?: string }) => {
let results = [...data.values()];
if (filter?.status) results = results.filter(c => c.status === filter.status);
return results;
},
updateCustomer: async (id: string, updates: Record<string, unknown>) => {
const existing = data.get(id);
if (!existing) return null;
const updated = { ...existing, ...updates };
data.set(id, updated);
return updated;
},
// Reset to initial state between tests
reset: () => {
data.clear();
for (const c of customers) data.set(c.id, { ...c });
},
};
}
- Define fixture data per entity Create 10-20 records per entity type covering active, inactive, archived, edge-case, and empty-field states. Use descriptive names for readability.
- Implement the real interface Your fixture must implement the same TypeScript interface as your production dependency. This guarantees fixture and production are interchangeable.
- Add reset for test isolation Every fixture gets a reset() method that restores initial state. Call it in beforeEach to prevent test pollution.