Skip to content
Production

Testing

Official Subatom.js documentation for third-party developers.

Testing

Comprehensive guide for testing Subatom applications using Vitest with integration and unit test examples.

Testing Setup

Dependencies

npm install -D vitest @vitest/ui
npm install -D node-fetch supertest  # HTTP testing
npm install -D @testing-library/node  # Utilities

Configuration

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

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    setupFiles: ["./tests/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      exclude: [
        "node_modules/",
        "tests/",
        "dist/",
      ],
    },
  },
});

Test Setup

// tests/setup.ts
import { beforeAll, afterAll } from "vitest";
import { Subatom } from "subatom";

// Global test server instance
let app: Subatom;
let server: Awaited<ReturnType<Subatom["listen"]>>;

beforeAll(async () => {
  app = new Subatom();
  // Register routes and middleware here.
  server = await app.listen(0);
});

afterAll(async () => {
  app.gracefulShutdown();
});

export { app, server };

Unit Testing

Testing Route Handlers

// tests/routes/users.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";
import usersRouter from "../../src/routes/users";

describe("Users Routes", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();
    app.use("/users", usersRouter);
  });

  describe("GET /users", () => {
    it("should return list of users", async () => {
      const response = await request(server).get("/users")
        .expect(200);

      expect(response.body).toBeInstanceOf(Array);
      expect(response.body.length).toBeGreaterThan(0);
    });

    it("should filter by email", async () => {
      const response = await request(server).get("/users?email=john@example.com")
        .expect(200);

      expect(response.body).toHaveLength(1);
      expect(response.body[0].email).toBe("john@example.com");
    });
  });

  describe("POST /users", () => {
    it("should create user", async () => {
      const response = await request(server).post("/users")
        .send({
          email: "newuser@example.com",
          name: "New User",
        })
        .expect(201);

      expect(response.body).toHaveProperty("id");
      expect(response.body.email).toBe("newuser@example.com");
    });

    it("should validate email format", async () => {
      const response = await request(server).post("/users")
        .send({
          email: "invalid-email",
          name: "User",
        })
        .expect(422);

      expect(response.body).toHaveProperty("error");
      expect(response.body.code).toBe("VALIDATION_ERROR");
    });
  });
});

Testing Middleware

// tests/middleware/auth.test.ts
import { describe, it, expect, vi } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";
import { requireAuth } from "../../src/middleware/auth";

describe("Auth Middleware", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();
    app.use(requireAuth);
    app.get("/protected", {
      controller: (ctx) => {
        ctx.res.json({ message: "Protected resource", user: ctx.user });
      },
    });
  });

  it("should reject request without token", async () => {
    const response = await request(server)
      .get("/protected")
      .expect(401);

    expect(response.body).toHaveProperty("error");
  });

  it("should accept request with valid token", async () => {
    const validToken = generateTestToken({ id: "user-1" });

    const response = await request(server)
      .get("/protected")
      .set("Authorization", `Bearer ${validToken}`)
      .expect(200);

    expect(response.body.user).toHaveProperty("id", "user-1");
  });

  it("should reject invalid token", async () => {
    const response = await request(server)
      .get("/protected")
      .set("Authorization", "Bearer invalid-token")
      .expect(401);

    expect(response.body).toHaveProperty("error");
  });
});

Testing Error Handlers

// tests/errors/handlers.test.ts
import { describe, it, expect } from "vitest";
import { Subatom, NotFoundError, BadRequestError } from "subatom";
import request from "supertest";

describe("Error Handlers", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();

    app.get("/notfound", {
      controller: () => {
        throw new NotFoundError("Resource not found");
      },
    });

    app.get("/badrequest", {
      controller: () => {
        throw new BadRequestError("Invalid request");
      },
    });

    app.useError((err, req, res) => {
      if (err instanceof NotFoundError) {
        res.status(404).json({ error: err.message, code: "NOT_FOUND" });
        return;
      }
      if (err instanceof BadRequestError) {
        res.status(400).json({ error: err.message, code: "BAD_REQUEST" });
        return;
      }
      res.status(500).json({ error: "Internal Server Error" });
    });
  });

  it("should handle NotFoundError", async () => {
    const response = await request(server)
      .get("/notfound")
      .expect(404);

    expect(response.body.code).toBe("NOT_FOUND");
  });

  it("should handle BadRequestError", async () => {
    const response = await request(server)
      .get("/badrequest")
      .expect(400);

    expect(response.body.code).toBe("BAD_REQUEST");
  });
});

Integration Testing

End-to-End Route Testing

// tests/integration/users.integration.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";
import infer from "subatom-infer";

const userSchema = infer.object({
  id: infer.string(),
  email: infer.string().email(),
  name: infer.string(),
});

const mockDb = {
  users: [
    { id: "1", email: "john@example.com", name: "John Doe" },
  ],
  
  async getUsers() {
    return this.users;
  },
  
  async getUserById(id: string) {
    return this.users.find(u => u.id === id);
  },
  
  async createUser(data: any) {
    const user = { id: String(Date.now()), ...data };
    this.users.push(user);
    return user;
  },
};

describe("Users Integration Tests", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();

    app.get("/users", {
      controller: async (ctx) => {
        const users = await mockDb.getUsers();
        ctx.res.json(users);
      },
    });

    app.get("/users/:id", {
      schema: { params: infer.object({ id: infer.string() }) },
      controller: async (ctx) => {
        const user = await mockDb.getUserById(ctx.params.id);
        if (!user) {
          ctx.res.status(404).json({ error: "Not found" });
          return;
        }
        ctx.res.json(user);
      },
    });

    app.post("/users", {
      schema: {
        body: infer.object({
          email: infer.string().email(),
          name: infer.string(),
        }),
      },
      controller: async (ctx) => {
        const user = await mockDb.createUser(ctx.body);
        ctx.res.status(201).json(user);
      },
    });
  });

  it("should complete full user lifecycle", async () => {
    // Create user
    const createRes = await request(server)
      .post("/users")
      .send({ email: "jane@example.com", name: "Jane Doe" })
      .expect(201);

    const userId = createRes.body.id;
    expect(createRes.body).toMatchObject({
      email: "jane@example.com",
      name: "Jane Doe",
    });

    // Get user
    const getRes = await request(server)
      .get(`/users/${userId}`)
      .expect(200);

    expect(getRes.body).toMatchObject({
      id: userId,
      email: "jane@example.com",
      name: "Jane Doe",
    });

    // List users includes new user
    const listRes = await request(server)
      .get("/users")
      .expect(200);

    expect(listRes.body).toHaveLength(2);
    expect(listRes.body).toContainEqual(expect.objectContaining({
      id: userId,
      email: "jane@example.com",
    }));
  });
});

Database Integration Tests

// tests/integration/database.test.ts
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Pool } from "pg";

const pool = new Pool({
  connectionString: "postgresql://localhost/test_db",
});

describe("Database Integration", () => {
  beforeEach(async () => {
    // Setup test database
    await pool.query(`
      DROP TABLE IF EXISTS users CASCADE;
      CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        email VARCHAR UNIQUE NOT NULL,
        name VARCHAR NOT NULL,
        created_at TIMESTAMP DEFAULT NOW()
      );
    `);
  });

  afterEach(async () => {
    await pool.query("DROP TABLE IF EXISTS users CASCADE");
  });

  it("should insert and retrieve users", async () => {
    // Insert
    const result = await pool.query(
      "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
      ["john@example.com", "John Doe"]
    );

    const userId = result.rows[0].id;

    // Retrieve
    const retrieved = await pool.query(
      "SELECT * FROM users WHERE id = $1",
      [userId]
    );

    expect(retrieved.rows[0]).toMatchObject({
      email: "john@example.com",
      name: "John Doe",
    });
  });
});

Snapshot Testing

// tests/responses.test.ts
import { describe, it, expect } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";

describe("Response Snapshots", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();
    app.get("/config", {
      controller: (ctx) => {
        ctx.res.json({
          version: "1.0.0",
          features: ["auth", "logging", "validation"],
        });
      },
    });
  });

  it("should match config snapshot", async () => {
    const response = await request(server)
      .get("/config")
      .expect(200);

    expect(response.body).toMatchSnapshot();
  });
});

Mocking

Mocking External Services

// tests/services/payment.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";

// Mock Stripe
const mockStripe = {
  createPaymentIntent: vi.fn(),
};

vi.mock("stripe", () => ({
  Stripe: () => mockStripe,
}));

describe("Payment Service", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();
    vi.clearAllMocks();
  });

  it("should create payment intent", async () => {
    mockStripe.createPaymentIntent.mockResolvedValue({
      id: "pi_test",
      amount: 1000,
      status: "succeeded",
    });

    app.post("/payments", {
      controller: async (ctx) => {
        const intent = await mockStripe.createPaymentIntent({
          amount: ctx.body.amount,
        });
        ctx.res.json(intent);
      },
    });

    const response = await request(server)
      .post("/payments")
      .send({ amount: 1000 })
      .expect(200);

    expect(response.body.id).toBe("pi_test");
    expect(mockStripe.createPaymentIntent).toHaveBeenCalledWith({
      amount: 1000,
    });
  });
});

Parametrized Testing

// tests/validation/email.test.ts
import { describe, it, expect } from "vitest";
import infer from "subatom-infer";

const emailSchema = infer.string().email();

describe("Email Validation", () => {
  const testCases = [
    { email: "valid@example.com", valid: true },
    { email: "user+tag@example.co.uk", valid: true },
    { email: "invalid.email@", valid: false },
    { email: "spaces in@example.com", valid: false },
    { email: "@example.com", valid: false },
  ];

  testCases.forEach(({ email, valid }) => {
    it(`should ${valid ? "accept" : "reject"} "${email}"`, () => {
      const result = emailSchema.safeParse(email);
      expect(result.success).toBe(valid);
    });
  });
});

Performance Testing

// tests/performance/routes.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { Subatom } from "subatom";
import request from "supertest";

describe("Performance Tests", () => {
  let app: Subatom;

  beforeEach(() => {
    app = new Subatom();
    app.get("/fast", {
      controller: (ctx) => {
        ctx.res.json({ message: "Fast" });
      },
    });
  });

  it("should respond quickly", async () => {
    const start = Date.now();

    await request(server)
      .get("/fast")
      .expect(200);

    const duration = Date.now() - start;
    expect(duration).toBeLessThan(100);  // Less than 100ms
  });

  it("should handle concurrent requests", async () => {
    const promises = Array.from({ length: 100 }, () =>
      request(server)get("/fast")
    );

    const start = Date.now();
    const responses = await Promise.all(promises);
    const duration = Date.now() - start;

    expect(responses).toHaveLength(100);
    expect(responses.every(r => r.status === 200)).toBe(true);
    expect(duration).toBeLessThan(1000);  // 100 requests in less than 1s
  });
});

Best Practices

1. Test File Organization

tests/
├── unit/
│   ├── routes/
│   ├── middleware/
│   └── services/
├── integration/
│   ├── database.test.ts
│   └── external-api.test.ts
├── fixtures/
│   ├── users.json
│   └── posts.json
└── setup.ts

2. Use Test Fixtures

// tests/fixtures/users.ts
export const mockUsers = [
  { id: "1", email: "john@example.com", name: "John" },
  { id: "2", email: "jane@example.com", name: "Jane" },
];

// tests/routes/users.test.ts
import { mockUsers } from "../fixtures/users";

3. Clean Up After Tests

afterEach(async () => {
  // Clean database
  await db.query("DELETE FROM users");
  
  // Reset mocks
  vi.clearAllMocks();
});

Running Tests

# Run all tests
npm test

# Run specific file
npm test -- tests/routes/users.test.ts

# Watch mode
npm test -- --watch

# Coverage
npm test -- --coverage

# UI mode
npm test -- --ui

Next Steps