Testing Professionale
Introduzione
Un server MCP professionale ha tre livelli di test: unit test sullo store, integration test sui tool via test harness, e wiring test per la comunicazione cross-server.
Piramide dei Test MCP
/\
/ \
/ W \ Wiring Test
/ I R \ (cross-server con InMemoryTransport)
/ I I \
/ N N G \
/────────────\
/ INTEGRATION \ Tool Test
/ (test harness) \ (tool → store → risultato)
/──────────────────\
/ \
/ UNIT TEST \ Store Test
/ (store in-memory) \ (logica pura, nessun MCP)
/──────────────────────────\
[object Object],[object Object],[object Object],[object Object]
undefined
Unit Test dello Store
Testano la logica di persistenza isolata, senza MCP:
import { describe, it, expect, beforeEach } from "vitest";
import { NotesStore } from "../../src/services/notes-store.js";
describe("NotesStore", () => {
let store: NotesStore;
beforeEach(() => {
// Ogni test ha un database fresco in-memory
store = new NotesStore({ inMemory: true });
});
it("should add and retrieve a note", () => {
const note = store.addNote({
title: "Test",
content: "Contenuto di test",
tags: ["tag1", "tag2"],
});
expect(note.id).toBe(1);
expect(note.title).toBe("Test");
expect(note.tags).toEqual(["tag1", "tag2"]);
const retrieved = store.getNote(1);
expect(retrieved).toEqual(note);
});
it("should return undefined for non-existent note", () => {
const note = store.getNote(999);
expect(note).toBeUndefined();
});
it("should list notes ordered by updatedAt desc", () => {
store.addNote({ title: "Prima", content: "A" });
store.addNote({ title: "Seconda", content: "B" });
store.addNote({ title: "Terza", content: "C" });
const notes = store.listNotes();
expect(notes).toHaveLength(3);
expect(notes[0].title).toBe("Terza"); // Piu' recente
});
it("should handle UNIQUE constraint on title", () => {
store.addNote({ title: "Unico", content: "A" });
expect(() => store.addNote({ title: "Unico", content: "B" })).toThrow();
});
it("should delete a note and return true", () => {
store.addNote({ title: "Da cancellare", content: "X" });
expect(store.deleteNote(1)).toBe(true);
expect(store.getNote(1)).toBeUndefined();
});
it("should return false when deleting non-existent note", () => {
expect(store.deleteNote(999)).toBe(false);
});
it("should search notes by content", () => {
store.addNote({ title: "JS", content: "Arrow functions e closures" });
store.addNote({ title: "TS", content: "Tipi generici e interfacce" });
const results = store.searchNotes("functions");
expect(results).toHaveLength(1);
expect(results[0].title).toBe("JS");
});
it("should update note content", () => {
store.addNote({ title: "Aggiornabile", content: "Vecchio" });
const updated = store.updateNote(1, { content: "Nuovo" });
expect(updated?.content).toBe("Nuovo");
});
});
Best Practice per Unit Test Store
- Usa
beforeEachconinMemory: trueper isolamento totale - Testa CRUD completo: create, read, update, delete
- Testa edge case: record inesistente, constraint violati, filtri vuoti
- Testa serializzazione/deserializzazione JSON (array, oggetti)
- Non testare query SQL direttamente, testa il comportamento
Integration Test dei Tool
Testano il tool end-to-end tramite il protocollo MCP, usando createTestHarness():
import { describe, it, expect, afterEach } from "vitest";
import { createTestHarness, MockEventBus, type TestHarness } from "@mcp-suite/testing";
import { createNotesServer } from "../../src/server.js";
describe("add-note tool", () => {
let harness: TestHarness;
afterEach(async () => {
if (harness) await harness.close();
});
it("should add a note and return it as JSON", async () => {
const { server } = createNotesServer({ storeOptions: { inMemory: true } });
harness = await createTestHarness(server);
const result = await harness.client.callTool({
name: "add-note",
arguments: {
title: "Test Note",
content: "Hello World",
tags: ["test"],
},
});
// Verifica formato risultato MCP
const content = result.content as Array<{ type: string; text: string }>;
expect(content[0].type).toBe("text");
// Verifica contenuto
const note = JSON.parse(content[0].text);
expect(note.title).toBe("Test Note");
expect(note.content).toBe("Hello World");
expect(note.tags).toEqual(["test"]);
expect(note.id).toBeDefined();
});
it("should return error for duplicate title", async () => {
const { server } = createNotesServer({ storeOptions: { inMemory: true } });
harness = await createTestHarness(server);
await harness.client.callTool({
name: "add-note",
arguments: { title: "Duplicato", content: "Primo" },
});
const result = await harness.client.callTool({
name: "add-note",
arguments: { title: "Duplicato", content: "Secondo" },
});
expect(result.isError).toBe(true);
});
it("should publish event when note is added", async () => {
const eventBus = new MockEventBus();
const { server } = createNotesServer({
eventBus,
storeOptions: { inMemory: true },
});
harness = await createTestHarness(server);
await harness.client.callTool({
name: "add-note",
arguments: { title: "Evento", content: "Test" },
});
expect(eventBus.wasPublished("notes:created")).toBe(true);
const events = eventBus.getPublishedEvents("notes:created");
expect(events[0].payload).toMatchObject({ title: "Evento" });
});
});
createTestHarness()
Questa utility crea una coppia client-server collegata in-memory:
export async function createTestHarness(server: McpServer): Promise<TestHarness> {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "1.0.0" });
await server.connect(serverTransport); // Server PRIMA
await client.connect(clientTransport); // Client DOPO
return {
client,
close: async () => {
await client.close();
await server.close();
},
};
}
Best Practice per Integration Test
- Testa il "happy path" e i casi di errore
- Verifica il formato del risultato MCP (content[0].type, isError)
- Verifica gli eventi pubblicati con MockEventBus
- Usa
afterEachper chiudere il harness (evita leak di risorse) - Non testare la logica dello store qui (gia' coperta dagli unit test)
Wiring Test Cross-Server
Testano la comunicazione reale tra server:
import { describe, it, expect, afterEach } from "vitest";
import { createTestHarness, type TestHarness } from "@mcp-suite/testing";
import { McpClientManager } from "@mcp-suite/client-manager";
import { createInsightEngineServer } from "../../src/server.js";
import { createAgileMetricsServer } from "../../../agile-metrics/src/server.js";
describe("insight-engine -> agile-metrics wiring", () => {
let callerHarness: TestHarness;
let clientManager: McpClientManager;
afterEach(async () => {
if (callerHarness) await callerHarness.close();
if (clientManager) await clientManager.disconnectAll();
});
it("should fetch velocity from agile-metrics", async () => {
// 1. Target server
const targetSuite = createAgileMetricsServer({
storeOptions: { inMemory: true },
});
// 2. Wire al ClientManager
clientManager = new McpClientManager();
const [ct, st] = McpClientManager.createInMemoryPair();
await targetSuite.server.connect(st);
await clientManager.connectInMemoryWithTransport("agile-metrics", ct);
// 3. Caller server
const callerSuite = createInsightEngineServer({
clientManager,
storeOptions: { inMemory: true },
});
callerHarness = await createTestHarness(callerSuite.server);
// 4. Chiama tool che fa cross-server
const result = await callerHarness.client.callTool({
name: "health-dashboard",
arguments: {},
});
// 5. Verifica
const content = result.content as Array<{ type: string; text: string }>;
const dashboard = JSON.parse(content[0].text);
expect(dashboard.dataSources["agile-metrics"]).toBe("available");
});
});
Struttura di un Wiring Test
Fase Azione Commento
─────────────────────────────────────────────────────────
1. Crea target server (in-memory) Server che viene chiamato
2. Crea InMemoryTransport pair [clientT, serverT]
3. target.server.connect(serverT) Server PRIMA
4. clientManager.connect(clientT) Client DOPO
5. (Opzionale) Popola dati target Via clientManager.callTool()
6. Crea caller con clientManager Server che chiama
7. createTestHarness(caller) Per invocare tool
8. Invoca tool cross-server Via harness.client.callTool()
9. Verifica risultato Assert sul contenuto
Organizzazione dei File di Test
servers/my-server/
tests/
services/
my-store.test.ts # Unit test store
tools/
add-item.test.ts # Integration test tool
get-stats.test.ts
get-stats-wiring.test.ts # Wiring test cross-server
server.test.ts # Test factory (opzionale)
Configurazione Vitest:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
include: ["tests/**/*.test.ts"],
},
});
Riepilogo
In questo capitolo hai imparato:
- La piramide dei test MCP: unit, integration, wiring
- Come testare lo store in isolamento con
inMemory: true createTestHarness()per test integration end-to-endMockEventBusper verificare eventi pubblicati- Pattern completo di wiring test con InMemoryTransport + ClientManager
- Organizzazione dei file di test per server
Prossimo: Best Practice e Produzione