Testing Guide
Seed uses Vitest for fast, modern testing with TypeScript support. This guide covers writing and running tests effectively.
Test Framework
Vitest Configuration
Configuration file: vitest.config.ts
typescript
export default defineConfig({
test: {
globals: true, // Use global test APIs
environment: "node", // Node environment
include: ["src/**/*.test.ts"], // Test file pattern
coverage: {
provider: "v8", // V8 coverage provider
reporter: ["text", "json", "html"], // Coverage formats
include: ["src/**/*.ts"], // Files to cover
exclude: ["src/**/*.test.ts", "src/types/**/*.ts"],
},
},
});Running Tests
| Command | Purpose |
|---|---|
npm test | Run all tests once |
npm run test:watch | Run tests in watch mode |
npm run test:coverage | Run tests with coverage report |
Running specific tests:
bash
# Run tests matching pattern
npx vitest run -t "auth middleware"
# Run specific test file
npx vitest run src/middleware/auth.test.ts
# Run tests in specific directory
npx vitest run src/routes/Test Structure
File Organization
Tests are co-located with source files:
src/
├── middleware/
│ ├── auth.ts # Implementation
│ └── auth.test.ts # Tests
├── routes/
│ ├── health.ts
│ └── health.test.ts
└── services/
├── redis.ts
└── redis.test.tsTest File Template
typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
describe("Feature name", () => {
// Setup before each test
beforeEach(() => {
// Initialize mocks, reset state
});
// Cleanup after each test
afterEach(() => {
// Clear mocks, restore state
});
it("should do something specific", () => {
// Arrange: Set up test data
const input = "test";
// Act: Execute the code
const result = functionUnderTest(input);
// Assert: Verify the result
expect(result).toBe("expected");
});
it("should handle error cases", () => {
expect(() => functionUnderTest(null)).toThrow("error message");
});
});Unit Testing
Testing Pure Functions
typescript
// src/utils/helpers.ts
export function generateClientId(prefix: string): string {
const random = randomBytes(6).toString("base64url");
return `${prefix}${random}`;
}
// src/utils/helpers.test.ts
import { describe, it, expect } from "vitest";
import { generateClientId } from "./helpers.js";
describe("generateClientId", () => {
it("should generate ID with correct prefix", () => {
const clientId = generateClientId("seed-");
expect(clientId).toMatch(/^seed-[a-zA-Z0-9_-]{8}$/);
});
it("should generate unique IDs", () => {
const id1 = generateClientId("seed-");
const id2 = generateClientId("seed-");
expect(id1).not.toBe(id2);
});
});Testing with Mocks
typescript
// src/services/client-store.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Redis } from "ioredis";
import { ClientStore } from "./client-store.js";
// Mock Redis module
vi.mock("ioredis");
describe("ClientStore", () => {
let mockRedis: Redis;
let store: ClientStore;
beforeEach(() => {
// Create mock Redis client
mockRedis = {
get: vi.fn(),
setex: vi.fn(),
del: vi.fn(),
} as unknown as Redis;
store = new ClientStore(mockRedis);
});
it("should save client to Redis", async () => {
const client = {
client_id: "seed-abc123",
client_name: "Test Client",
};
await store.save(client);
expect(mockRedis.setex).toHaveBeenCalledWith(
"dcr:client:seed-abc123",
2592000,
JSON.stringify(client)
);
});
it("should retrieve client from Redis", async () => {
const clientData = JSON.stringify({
client_id: "seed-abc123",
client_name: "Test Client",
});
vi.mocked(mockRedis.get).mockResolvedValue(clientData);
const client = await store.get("seed-abc123");
expect(client).toEqual({
client_id: "seed-abc123",
client_name: "Test Client",
});
expect(mockRedis.get).toHaveBeenCalledWith("dcr:client:seed-abc123");
});
it("should return null for non-existent client", async () => {
vi.mocked(mockRedis.get).mockResolvedValue(null);
const client = await store.get("seed-nonexistent");
expect(client).toBeNull();
});
});Testing MCP Tools
typescript
// src/mcp/tools/seed_ping.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerSeedPing } from "./seed_ping.js";
// Mock configuration
vi.mock("../../config/index.js", () => ({
config: {
server: {
version: "0.1.0",
},
},
}));
describe("seed_ping tool", () => {
let server: McpServer;
let registeredHandler: () => Promise<{ content: { type: string; text: string }[] }>;
beforeEach(() => {
// Mock MCP server
server = {
registerTool: vi.fn((name, options, handler) => {
registeredHandler = handler;
}),
} as unknown as McpServer;
registerSeedPing(server);
});
it("should register the seed_ping tool", () => {
expect(server.registerTool).toHaveBeenCalledWith(
"seed_ping",
{ description: "Health check tool - returns server status" },
expect.any(Function)
);
});
it("should return status ok with version", async () => {
const result = await registeredHandler();
expect(result.content).toHaveLength(1);
const content = result.content[0];
expect(content?.type).toBe("text");
const parsed = JSON.parse(content?.text ?? "{}");
expect(parsed.status).toBe("ok");
expect(parsed.version).toBe("0.1.0");
});
it("should include valid ISO timestamp", async () => {
const before = new Date();
const result = await registeredHandler();
const after = new Date();
const content = result.content[0];
const parsed = JSON.parse(content?.text ?? "{}");
const timestamp = new Date(parsed.timestamp);
expect(timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
});
});Integration Testing
Testing HTTP Endpoints
Use Supertest for HTTP endpoint testing:
typescript
// src/routes/health.test.ts
import { describe, it, expect, vi } from "vitest";
import request from "supertest";
import express from "express";
import { healthRouter } from "./health.js";
// Mock configuration
vi.mock("../config/index.js", () => ({
config: {
server: {
version: "0.1.0",
},
},
}));
describe("/health endpoint", () => {
const app = express();
app.use("/health", healthRouter);
it("should return 200 OK", async () => {
const response = await request(app).get("/health");
expect(response.status).toBe(200);
});
it("should return status ok", async () => {
const response = await request(app).get("/health");
expect(response.body.status).toBe("ok");
});
it("should return the server version", async () => {
const response = await request(app).get("/health");
expect(response.body.version).toBe("0.1.0");
});
it("should return JSON content type", async () => {
const response = await request(app).get("/health");
expect(response.headers["content-type"]).toMatch(/application\/json/);
});
});Testing Middleware
typescript
// src/middleware/auth.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Request, Response, NextFunction } from "express";
import { authMiddleware } from "./auth.js";
describe("authMiddleware", () => {
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
let mockNext: NextFunction;
beforeEach(() => {
mockReq = {
path: "/mcp",
headers: {},
};
mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
setHeader: vi.fn(),
};
mockNext = vi.fn();
});
it("should call next() for public paths", async () => {
mockReq.path = "/health";
await authMiddleware(
mockReq as Request,
mockRes as Response,
mockNext
);
expect(mockNext).toHaveBeenCalled();
});
it("should return 401 for missing Authorization header", async () => {
await authMiddleware(
mockReq as Request,
mockRes as Response,
mockNext
);
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
code: -32001,
message: "Unauthorized",
}),
})
);
});
it("should validate Bearer token format", async () => {
mockReq.headers = { authorization: "InvalidFormat" };
await authMiddleware(
mockReq as Request,
mockRes as Response,
mockNext
);
expect(mockRes.status).toHaveBeenCalledWith(401);
});
});Testing with Async Operations
typescript
describe("async operations", () => {
it("should handle async functions", async () => {
const result = await someAsyncFunction();
expect(result).toBeDefined();
});
it("should handle promise rejections", async () => {
await expect(failingAsyncFunction()).rejects.toThrow("error");
});
it("should timeout long operations", async () => {
const promise = longRunningOperation();
await expect(promise).rejects.toThrow("timeout");
}, 5000); // 5 second timeout
});Test Patterns
AAA Pattern (Arrange, Act, Assert)
typescript
it("should calculate total correctly", () => {
// Arrange: Set up test data
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 },
];
// Act: Execute the function
const total = calculateTotal(items);
// Assert: Verify the result
expect(total).toBe(35);
});Testing Error Conditions
typescript
it("should throw error for invalid input", () => {
expect(() => validateInput(null)).toThrow("Input cannot be null");
expect(() => validateInput("")).toThrow("Input cannot be empty");
expect(() => validateInput(-1)).toThrow("Input must be positive");
});Testing Type Guards
typescript
function isUser(obj: unknown): obj is User {
return typeof obj === "object" && obj !== null && "id" in obj;
}
it("should correctly identify User objects", () => {
expect(isUser({ id: 1, name: "Test" })).toBe(true);
expect(isUser({})).toBe(false);
expect(isUser(null)).toBe(false);
expect(isUser("string")).toBe(false);
});Snapshot Testing
typescript
it("should match snapshot", () => {
const result = generateComplexObject();
expect(result).toMatchSnapshot();
});
// Update snapshots with: npx vitest run -uMocking Best Practices
Module Mocking
typescript
// Mock entire module
vi.mock("./module.js", () => ({
functionA: vi.fn(),
functionB: vi.fn(),
}));
// Mock specific exports
vi.mock("./module.js", async () => {
const actual = await vi.importActual("./module.js");
return {
...actual,
specificFunction: vi.fn(),
};
});Spy on Functions
typescript
import { vi } from "vitest";
import * as module from "./module.js";
it("should call function", () => {
const spy = vi.spyOn(module, "functionName");
module.functionName("arg");
expect(spy).toHaveBeenCalledWith("arg");
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});Mock Return Values
typescript
const mockFn = vi.fn();
// Single return value
mockFn.mockReturnValue("value");
// Async return value
mockFn.mockResolvedValue("async value");
// Sequential return values
mockFn
.mockReturnValueOnce("first")
.mockReturnValueOnce("second")
.mockReturnValue("default");
// Throw error
mockFn.mockRejectedValue(new Error("failure"));Coverage Requirements
Running Coverage
bash
npm run test:coverageOutput formats:
- Terminal summary
- HTML report:
coverage/index.html - JSON data:
coverage/coverage-final.json
Coverage Configuration
typescript
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts", // Test files
"src/types/**/*.ts", // Type definitions
],
}Coverage Thresholds
While not enforced in CI yet, aim for:
- Statements: 80%+
- Branches: 75%+
- Functions: 80%+
- Lines: 80%+
Debugging Tests
Using Console Logging
typescript
it("should debug test", () => {
console.log("Debug value:", value);
expect(value).toBe(expected);
});VS Code Debugging
Add .vscode/launch.json:
json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "test:watch"],
"console": "integratedTerminal"
}
]
}Isolating Tests
typescript
// Run only this test
it.only("should run only this test", () => {
// ...
});
// Skip this test
it.skip("should skip this test", () => {
// ...
});
// Run only this describe block
describe.only("Feature", () => {
// ...
});Common Testing Pitfalls
1. Not Cleaning Up Mocks
Problem:
typescript
it("test 1", () => {
vi.mock("./module.js");
// Test runs
});
it("test 2", () => {
// Mock still active from test 1
});Solution:
typescript
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});2. Testing Implementation Details
Bad:
typescript
it("should call internal function", () => {
const spy = vi.spyOn(service, "_internalMethod");
service.publicMethod();
expect(spy).toHaveBeenCalled();
});Good:
typescript
it("should return correct result", () => {
const result = service.publicMethod();
expect(result).toBe(expected);
});3. Flaky Tests
Causes:
- Time-dependent tests
- Race conditions
- External dependencies
- Random data
Solutions:
- Mock
Date.now()and timers - Use
awaitfor async operations - Mock external services
- Use deterministic test data
Writing Testable Code
Dependency Injection
Bad:
typescript
export function fetchData() {
const redis = new Redis(); // Hard to mock
return redis.get("key");
}Good:
typescript
export function fetchData(redis: Redis) {
return redis.get("key"); // Easy to mock
}Pure Functions
Bad:
typescript
let total = 0;
export function addToTotal(value: number) {
total += value; // Side effect
}Good:
typescript
export function calculateTotal(current: number, value: number): number {
return current + value; // Pure function
}Next Steps
- Code Quality - Linting and formatting standards
- Adding MCP Tools - Testing custom MCP tools
- Contributing Guide - Submitting tested code