Skip to content

Testing

Minima.js is built with testability in mind. This guide covers testing strategies for your Minima.js applications using popular testing frameworks.

Quick Reference

Setup

With Bun

Bun includes a built-in test runner:

bash
bun test

Example test file (app.test.ts):

ts
import { describe, test, expect } from "bun:test";
import { createApp } from "@minimajs/server/bun";

describe("App", () => {
  test("GET /health returns 200", async () => {
    const app = createApp();
    app.get("/health", () => ({ status: "ok" }));

    const response = await app.handle(new Request("http://localhost/health"));
    expect(response.status).toBe(200);

    const data = await response.json();
    expect(data).toEqual({ status: "ok" });
  });
});

Tip: Use createRequest() from @minimajs/server/mock for cleaner test code, or use native new Request() for more control.

With Jest/Vitest

Install dependencies:

bash
npm install --save-dev vitest
# or
npm install --save-dev jest @types/jest

Configure (vitest.config.ts):

ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
  },
});

Unit Testing

Test individual handlers in isolation.

Testing Route Handlers

ts
import { describe, test, expect } from "bun:test";
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { params, body } from "@minimajs/server";

describe("User Routes", () => {
  test("GET /users/:id returns user", async () => {
    const app = createApp();

    app.get("/users/:id", () => {
      const id = params.get("id");
      return { id, name: "Alice" };
    });

    const response = await app.handle(createRequest("/users/123"));

    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data).toEqual({ id: "123", name: "Alice" });
  });

  test("POST /users creates user", async () => {
    const app = createApp();

    app.post("/users", () => {
      const userData = body();
      return { id: "456", ...userData };
    });

    const response = await app.handle(
      createRequest("/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "Bob" }),
      })
    );

    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data).toEqual({ id: "456", name: "Bob" });
  });
});
ts
import { describe, test, expect } from "vitest"; // or @jest/globals
import { createApp } from "@minimajs/server/node";
import { createRequest } from "@minimajs/server/mock";
import { params, body } from "@minimajs/server";

describe("User Routes", () => {
  test("GET /users/:id returns user", async () => {
    const app = createApp();

    app.get("/users/:id", () => {
      const id = params.get("id");
      return { id, name: "Alice" };
    });

    const response = await app.handle(createRequest("/users/123"));

    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data).toEqual({ id: "123", name: "Alice" });
  });

  // ... other tests
});

Alternative: You can also use native new Request("http://localhost/path", options) if you need more control over the request object.

Testing with Query Parameters

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { searchParams } from "@minimajs/server";

test("GET /search with query params", async () => {
  const app = createApp();

  app.get("/search", () => {
    const params = searchParams();
    const query = params.get("q");
    return { results: [], query };
  });

  const response = await app.handle(createRequest("/search?q=test"));

  const data = await response.json();
  expect(data.query).toBe("test");
});

Testing Headers

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { headers, response } from "@minimajs/server";

test("requires authorization header", async () => {
  const app = createApp();

  app.get("/protected", () => {
    const auth = headers().get("authorization");
    if (!auth) {
      response.status(401);
      return { error: "Unauthorized" };
    }
    return { data: "secret" };
  });

  // Without auth
  const res1 = await app.handle(createRequest("/protected"));
  expect(res1.status).toBe(401);

  // With auth
  const res2 = await app.handle(
    createRequest("/protected", {
      headers: { Authorization: "Bearer token" },
    })
  );
  expect(res2.status).toBe(200);
});

Integration Testing

Test the full application with plugins and middleware.

Testing with Plugins

ts
import { createApp } from "@minimajs/server/bun";
import { body } from "@minimajs/server";
import { createRequest } from "@minimajs/server/mock";
import { bodyParser } from "@minimajs/server/plugins";

test("full app with plugins", async () => {
  const app = createApp();

  app.register(bodyParser());

  app.post("/data", () => {
    const data = body();
    return { received: data };
  });

  const response = await app.handle(
    createRequest("/data", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ value: 42 }),
    })
  );

  const result = await response.json();
  expect(result.received).toEqual({ value: 42 });
});

Testing Module Encapsulation

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";

test("module routes are isolated", async () => {
  const app = createApp();

  app.register(
    async (adminApp) => {
      adminApp.get("/users", () => ({ users: [] }));
    },
    { prefix: "/admin" }
  );

  app.register(
    async (apiApp) => {
      apiApp.get("/users", () => ({ data: [] }));
    },
    { prefix: "/api" }
  );

  // Test admin module
  const res1 = await app.handle(createRequest("/admin/users"));
  const data1 = await res1.json();
  expect(data1).toHaveProperty("users");

  // Test api module
  const res2 = await app.handle(createRequest("/api/users"));
  const data2 = await res2.json();
  expect(data2).toHaveProperty("data");
});

Testing Hooks

Testing Request Hooks

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { hook, createContext } from "@minimajs/server";

test("request hook modifies context", async () => {
  const app = createApp();
  const [getRequestId, setRequestId] = createContext<string>();

  app.register(
    hook("request", () => {
      setRequestId("test-123");
    })
  );

  app.get("/", () => {
    const requestId = getRequestId();
    return { requestId };
  });

  const response = await app.handle(createRequest("/"));
  const data = await response.json();
  expect(data.requestId).toBe("test-123");
});

Testing Transform Hooks

ts
import { createApp } from "@minimajs/server/bun";
import { hook } from "@minimajs/server";
import { createRequest } from "@minimajs/server/mock";

test("transform hook wraps response", async () => {
  const app = createApp();

  app.register(
    hook("transform", (data) => {
      return { success: true, data };
    })
  );

  app.get("/users", () => [{ id: 1 }]);

  const response = await app.handle(createRequest("/users"));
  const data = await response.json();
  expect(data).toEqual({
    success: true,
    data: [{ id: 1 }],
  });
});

Testing Send Hooks

ts
import { createApp } from "@minimajs/server/bun";
import { hook, response } from "@minimajs/server";
import { createRequest } from "@minimajs/server/mock";

test("send hook adds custom header", async () => {
  const app = createApp();

  app.register(
    hook("send", (ctx) => {
      return response("hello world", {
        headers: { "X-Custom": "value" },
      });
    })
  );

  app.get("/", () => "ok");

  const response = await app.handle(createRequest("/"));
  expect(response.headers.get("X-Custom")).toBe("value");
});

Testing Plugins

Testing Custom Plugins

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { createContext, hook, plugin } from "@minimajs/server";

const [getPluginData, setPluginData] = createContext<string>();

const myPlugin = (options: { value: string }) =>
  plugin.sync((app) => {
    app.register(
      hook("request", () => {
        setPluginData(options.value);
      })
    );
  });

test("custom plugin sets context", async () => {
  const app = createApp();
  app.register(myPlugin({ value: "test" }));
  app.get("/", () => {
    return { data: getPluginData() };
  });

  const response = await app.handle(createRequest("/"));
  const data = await response.json();
  expect(data.data).toBe("test");
});

Mocking

Mocking External Services

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { mock } from "bun:test";

test("mocked database query", async () => {
  const mockDb = {
    query: mock(() => Promise.resolve([{ id: 1, name: "Alice" }])),
  };

  const app = createApp();

  app.get("/users", async () => {
    const users = await mockDb.query();
    return users;
  });

  const response = await app.handle(createRequest("/users"));
  const data = await response.json();

  expect(mockDb.query).toHaveBeenCalled();
  expect(data).toHaveLength(1);
});

Mocking Context Values

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";
import { createContext, hook } from "@minimajs/server";

test("mock context for testing", async () => {
  const app = createApp();
  const [getUserId, setUserId] = createContext<string>();

  // Set up context in request hook
  app.register(
    hook("request", () => {
      setUserId("mock-user-123");
    })
  );

  app.get("/profile", () => {
    const userId = getUserId();
    return { userId };
  });

  const response = await app.handle(createRequest("/profile"));
  const data = await response.json();
  expect(data.userId).toBe("mock-user-123");
});

Best Practices

1. Test in Isolation

Create a fresh app instance for each test:

ts
import { beforeEach } from "bun:test";

describe("User API", () => {
  let app;

  beforeEach(() => {
    app = createApp();
    // Setup routes
    app.get("/users", () => []);
  });

  test("test 1", async () => {
    // app is fresh
  });

  test("test 2", async () => {
    // app is fresh again
  });
});

2. Use Helper Functions

ts
// test-helpers.ts
import { createRequest } from "@minimajs/server/mock";

export async function makeRequest(app: App, path: string, options?: RequestInit) {
  const response = await app.handle(createRequest(path, options));
  const data = await response.json();
  return { response, data };
}

// In tests
test("using helper", async () => {
  const app = createApp();
  app.get("/", () => ({ ok: true }));

  const { response, data } = await makeRequest(app, "/");
  expect(data.ok).toBe(true);
});

3. Test Error Cases

ts
import { createApp } from "@minimajs/server/bun";
import { createRequest } from "@minimajs/server/mock";

test("handles errors gracefully", async () => {
  const app = createApp();

  app.get("/error", () => {
    throw new Error("Something went wrong");
  });

  const response = await app.handle(createRequest("/error"));
  expect(response.status).toBe(500);
});

4. Test Edge Cases

ts
import { createApp } from "@minimajs/server/bun";
import { body, params, response } from "@minimajs/server";
import { createRequest } from "@minimajs/server/mock";
import { bodyParser } from "@minimajs/server/plugins";

describe("Edge cases", () => {
  test("handles missing parameters", async () => {
    const app = createApp();
    app.get("/users/:id", () => {
      const id = params.get("id");
      if (!id) {
        response.status(400);
        return { error: "ID required" };
      }
      return { id };
    });

    const response = await app.handle(createRequest("/users/"));
    expect(response.status).toBe(404); // Route doesn't match
  });

  test("handles malformed JSON", async () => {
    const app = createApp();
    app.register(bodyParser());

    app.post("/data", () => {
      try {
        const data = body();
        return data;
      } catch (error) {
        response.status(400);
        return { error: "Invalid JSON" };
      }
    });

    const response = await app.handle(
      createRequest("/data", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: "{ invalid json }",
      })
    );

    expect(response.status).toBe(400);
  });
});

5. Coverage Goals

Aim for good coverage of:

  • ✅ Happy paths (expected inputs)
  • ✅ Error paths (invalid inputs, exceptions)
  • ✅ Edge cases (boundaries, empty values)
  • ✅ Integration points (plugins, hooks)