Skip to content

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.

typescript
import { spy } from "kyrin";

Basic Usage

typescript
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

typescript
const fn = spy(() => 0);

fn.mockReturnValue(42);
console.log(fn()); // 42

fn.mockReturnValue("hello");
console.log(fn()); // "hello"

Mocking Async Return Values

typescript
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

typescript
const fn = spy((x: number) => x * 2);

fn.mockImplementation((x: number) => x * 10);
console.log(fn(5)); // 50

// Can restore original implementation later

Resetting and Restoring

typescript
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 history

MockFunction<T> Interface

typescript
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

typescript
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.

typescript
import { stub } from "kyrin";

Basic Usage

typescript
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 CaseRecommended
Wrap a real function and track callsspy(fn)
Replace a function's return valuespy(fn).mockReturnValue(x)
Create a fake object for a typestub<T>()
Need to track method calls on a stubUse 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.

typescript
import { MockDatabase } from "kyrin";

Setting Up Data

typescript
const db = new MockDatabase();

db.setTableData("users", [
  { id: 1, name: "Alice", email: "[email protected]" },
  { id: 2, name: "Bob", email: "[email protected]" },
]);

Querying Data

typescript
// 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

typescript
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

typescript
db.clear(); // Removes all table data

Complete Example

typescript
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.

typescript
import { MockResponse } from "kyrin";

Building Responses

typescript
// 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

MethodDescription
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.

typescript
import { mockRequest } from "kyrin";

Basic Usage

typescript
// 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

typescript
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.

typescript
import { createMockContext } from "kyrin";

Basic Usage

typescript
const ctx = createMockContext();

console.log(ctx.method); // "GET"
console.log(ctx.path);   // "/"

Custom Context

typescript
const ctx = createMockContext({
  path: "/api/users/123",
  method: "POST",
  params: { id: "123" },
  body: { name: "Alice" },
});

Mock Context API

typescript
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 body

Complete Mocking Example

typescript
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

Released under the MIT License.