Mocking Utilities
Kyrin's testing toolkit includes powerful mocking utilities: spies, stubs, mock database, mock response, mock request, and mock context. These let you isolate and test specific parts of your application without real dependencies.
Spy
A spy wraps a function and lets you inspect how it was called, what arguments were passed, and what values were returned. It can also replace the function's behavior.
import { spy } from "kyrin";Basic Usage
function add(a: number, b: number) {
return a + b;
}
const mockAdd = spy(add);
const result = mockAdd(1, 2);
console.log(result); // 3
console.log(mockAdd.mock.calls); // [[1, 2]]
console.log(mockAdd.mock.results); // [{ value: 3 }]Mocking Return Values
const fn = spy(() => 0);
fn.mockReturnValue(42);
console.log(fn()); // 42
fn.mockReturnValue("hello");
console.log(fn()); // "hello"Mocking Async Return Values
const fn = spy(async () => "original");
fn.mockResolvedValue("mocked");
console.log(await fn()); // "mocked"
fn.mockRejectedValue(new Error("fail"));
try {
await fn();
} catch (e) {
console.log(e.message); // "fail"
}Custom Implementation
const fn = spy((x: number) => x * 2);
fn.mockImplementation((x: number) => x * 10);
console.log(fn(5)); // 50
// Can restore original implementation laterResetting and Restoring
const fn = spy((x: number) => x);
fn(1);
fn.mockReturnValue(42);
fn.reset(); // Clears call history only
fn.restore(); // Clears all mock configuration and call historyMockFunction<T> Interface
interface MockFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T>;
mock: {
calls: Parameters<T>[]; // All calls with their arguments
results: { value: ReturnType<T> }[]; // All return values
instances: unknown[]; // `this` context for each call
};
mockReturnValue(value: ReturnType<T>): this;
mockResolvedValue(value: Awaited<ReturnType<T>>): this;
mockRejectedValue(error: Error): this;
mockImplementation(fn: T): this;
reset(): void; // Clear call history
restore(): void; // Clear all mocks and call history
}Practical Example
import { spy, expect } from "kyrin";
// Real function that calls an API
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
const mockFetchUser = spy(fetchUser);
// Mock it to avoid real HTTP calls
mockFetchUser.mockResolvedValue({
id: "123",
name: "Alice",
email: "[email protected]",
});
const user = await mockFetchUser("123");
expect(user.name).toBe("Alice");
expect(mockFetchUser.mock.calls.length).toBe(1);
expect(mockFetchUser.mock.calls[0]).toEqual(["123"]);Stub
A stub creates a fake object where every property access returns a no-op function. This is useful when you need to pass an object that satisfies a type but doesn't need real behavior.
import { stub } from "kyrin";Basic Usage
interface UserService {
getUser(id: number): Promise<{ name: string }>;
saveUser(user: { name: string }): Promise<void>;
deleteUser(id: number): Promise<boolean>;
}
const service = stub<UserService>();
// All methods exist and are callable
await service.getUser(1);
await service.saveUser({ name: "Alice" });
await service.deleteUser(1);
// Track calls
console.log((service as any).mock.calls);When to Use Stub vs Spy
| Use Case | Recommended |
|---|---|
| Wrap a real function and track calls | spy(fn) |
| Replace a function's return value | spy(fn).mockReturnValue(x) |
| Create a fake object for a type | stub<T>() |
| Need to track method calls on a stub | Use spy(stub.method) or access (stub as any).mock.calls |
MockDatabase
MockDatabase simulates an in-memory database for testing query logic without a real database connection.
import { MockDatabase } from "kyrin";Setting Up Data
const db = new MockDatabase();
db.setTableData("users", [
{ id: 1, name: "Alice", email: "[email protected]" },
{ id: 2, name: "Bob", email: "[email protected]" },
]);Querying Data
// Get all records
const all = await db.from("users").all();
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
// Get the first record
const first = await db.from("users").first();
// { id: 1, name: "Alice" }
// Filter with where clause
const result = await db.from("users").where("id", 1).first();
// { id: 1, name: "Alice" }
const results = await db.from("users").where("name", "Alice").all();
// [{ id: 1, name: "Alice" }]Inserting Data
const newUser = await db.from("users").insert({
name: "Charlie",
email: "[email protected]",
});
// Auto-assigns an id (sequential): { id: 3, name: "Charlie", email: "[email protected]" }Clearing Data
db.clear(); // Removes all table dataComplete Example
import { MockDatabase, expect } from "kyrin";
test("MockDatabase queries", async () => {
const db = new MockDatabase();
db.setTableData("products", [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 25 },
{ id: 3, name: "Keyboard", price: 75 },
]);
const allProducts = await db.from("products").all();
expect(allProducts.length).toBe(3);
const cheapProducts = await db.from("products").where("price", 25).all();
expect(cheapProducts.length).toBe(1);
expect(cheapProducts[0].name).toBe("Mouse");
const newProduct = await db.from("products").insert({
name: "Monitor",
price: 299,
});
expect(newProduct.id).toBe(4);
});MockResponse
MockResponse is a builder for creating standard Web Response objects in tests.
import { MockResponse } from "kyrin";Building Responses
// JSON response
const jsonRes = new MockResponse()
.status(200)
.json({ message: "success" })
.header("X-Custom", "value")
.build();
// Text response
const textRes = new MockResponse()
.status(404)
.text("Not Found")
.build();
// Chained API
const res = new MockResponse()
.status(201)
.json({ id: 1 })
.header("Location", "/users/1")
.build();Methods
| Method | Description |
|---|---|
status(code) | Set HTTP status code |
json(data) | Set JSON body (auto-sets Content-Type: application/json) |
text(data) | Set text body (auto-sets Content-Type: text/plain) |
header(key, value) | Add a response header |
build() | Return a standard Response object |
MockRequest
MockRequest creates a standard Web Request object with sensible defaults for testing.
import { mockRequest } from "kyrin";Basic Usage
// Default GET request
const req = mockRequest();
console.log(req.method); // "GET"
console.log(req.url); // "http://localhost/"
// Custom request
const req = mockRequest({
method: "POST",
body: JSON.stringify({ name: "test" }),
headers: { Authorization: "Bearer token" },
});Parameters
interface MockRequestOptions {
method?: string; // Default: "GET"
body?: string; // Default: undefined
headers?: Record<string, string>; // Default: { "Content-Type": "application/json" }
}
const req = mockRequest({
method: "DELETE",
headers: { "X-API-Key": "secret" },
});createMockContext
createMockContext creates a minimal mock Context object that matches the shape Kyrin handlers receive. This is useful for testing handlers in isolation.
import { createMockContext } from "kyrin";Basic Usage
const ctx = createMockContext();
console.log(ctx.method); // "GET"
console.log(ctx.path); // "/"Custom Context
const ctx = createMockContext({
path: "/api/users/123",
method: "POST",
params: { id: "123" },
body: { name: "Alice" },
});Mock Context API
const ctx = createMockContext({
path: "/users/42",
method: "GET",
params: { id: "42" },
body: {},
});
// Request
ctx.req; // Standard Request object
ctx.method; // "GET"
ctx.path; // "/users/42"
// Parameters
ctx.params; // { id: "42" }
ctx.param("id"); // "42" (or null if not found)
ctx.query("page"); // null (query string not parsed by default)
// Store (for middleware data)
ctx.store; // {} — attach data during middleware
// Response helpers
ctx.set.status; // Current status (modifiable)
ctx.set.headers; // Current headers (modifiable)
ctx.json({ data: true }); // Returns JSON Response
ctx.json({ error: "bad" }, 400); // JSON Response with status
ctx.send("message"); // Returns text Response
ctx.send("not found", 404); // Text Response with status
ctx.notFound(); // Returns 404 Response
// Body parsing
const body = await ctx.body(); // Parsed request bodyComplete Mocking Example
import {
spy, stub, MockDatabase, MockResponse,
mockRequest, createMockContext, expect,
} from "kyrin";
describe("Mocking utilities", () => {
test("spy tracks calls and can mock return values", () => {
const fn = spy((x: number) => x * 2);
expect(fn(5)).toBe(10);
expect(fn.mock.calls).toEqual([[5]]);
fn.mockReturnValue(100);
expect(fn(1)).toBe(100);
});
test("stub creates typed fake objects", () => {
const api = stub<{ getUser: (id: number) => string }>();
api.getUser(1);
expect((api as any).mock.calls).toEqual([[1]]);
});
test("MockDatabase stores and queries data", async () => {
const db = new MockDatabase();
db.setTableData("users", [{ id: 1, name: "Alice" }]);
const users = await db.from("users").all();
expect(users.length).toBe(1);
expect(users[0].name).toBe("Alice");
});
test("MockResponse builds Response objects", () => {
const res = new MockResponse()
.status(201)
.json({ id: 1 })
.header("Location", "/items/1")
.build();
expect(res.status).toBe(201);
expect(res.headers.get("Location")).toBe("/items/1");
});
test("mockRequest creates Request objects", () => {
const req = mockRequest({
method: "POST",
body: '{"name":"test"}',
});
expect(req.method).toBe("POST");
});
test("createMockContext simulates handler context", async () => {
const ctx = createMockContext({
path: "/test",
method: "POST",
body: { key: "value" },
});
expect(ctx.method).toBe("POST");
expect(ctx.path).toBe("/test");
expect(await ctx.body()).toEqual({ key: "value" });
const res = ctx.json({ ok: true });
expect(res.status).toBe(200);
});
});Next Steps
- Getting Started — TestApp and TestClient
- Expect Helpers — Assertions:
expect(),expectStatus(),expectBody()