Testing

Harpia embraces a robust testing culture, relying on the blazingly fast, native bun:test runner. The framework focuses heavily on End-to-End (E2E) testing to ensure your routing, validation, business logic, and database operations all function seamlessly together.

The Test Client

To facilitate E2E testing without the overhead of spinning up a live HTTP server on an actual network port, Harpia provides a built-in TestClient within the @harpiats/core package.

The TestClient allows you to dispatch requests directly to your application instance.

// modules/user/tests/create.spec.ts
import { describe, expect, it } from "bun:test";
import { TestClient } from "@harpiats/core";
import { app } from "start/server";

const client = new TestClient(app);

describe("POST /users", () => {
  it("should create a new user", async () => {
    const response = await client
      .post("/users")
      .json({ name: "John Doe", email: "john@example.com", password: "password123" })
      .execute();

    const body = await response.json();

    expect(response.status).toBe(201);
    expect(body.data.email).toBe("john@example.com");
  });
});

[!NOTE] For a comprehensive guide on simulating headers, uploading files, and making complex assertions, please refer to the Test Client documentation.

Database Cleaning

When executing integration or E2E tests, it is critical that your database retains a predictable, clean state. However, truncating your entire database between tests can severely damage your foundational seed data and cause foreign key violations.

Harpia tackles this with a highly targeted TestCleaner from the @harpiats/common package. Instead of wiping tables, it performs a surgical garbage collection of the exact records you generate during your test block.

import { afterEach, describe, test } from "bun:test";
import { TestCleaner } from "@harpiats/common";
import { User } from "app/database";

// Instantiate the cleaner by injecting the model mapping
const cleaner = new TestCleaner({ User });

describe("User Module", () => {
  test("should test the user module", async () => {
    // 1. You register the generated record ID
    cleaner.register("User", 10);
  });
});

// 2. The cleaner automatically targets and deletes the registered ID after each test
afterEach(async () => await cleaner.clean());

[!NOTE] For a comprehensive guide on instantiating the cleaner globally and managing complex relational data during tests, please refer to the Test Cleaner documentation.

Writing Effective Tests

  1. Focus on E2E: Test the entire flow from the HTTP request down to the database using the TestClient.
  2. Utilise Factories: Use Factories to quickly generate complex, relational data required for your assertions.
  3. Assert Status Codes: Always assert that the returned HTTP status code matches your expectations (e.g., 201 Created vs 400 Bad Request).
  4. Isolate State: Never rely on data created by a previous test. Each test() or it() block should ideally arrange its own isolated data and clean it up afterwards.