Test Cleaner

When building End-to-End (E2E) tests, it is absolutely essential to ensure that each test runs in an isolated, predictable state. If one test creates a user and the next test expects an empty database, your test suite will become flaky and unreliable.

Traditional frameworks often solve this by executing a global truncate command across all tables before every test. However, this approach can destroy essential seed data and trigger foreign key violations.

Harpia solves this differently: the @harpiats/common package provides a highly surgical TestCleaner. Instead of wiping tables blindly, it acts as a focused garbage collector, explicitly tracking and deleting only the specific records you generated during a test block.

Instantiating the Cleaner

The TestCleaner class requires you to inject a “Model Map” via its constructor. This map links a string alias (e.g., "User") to the actual Prisma Client model, allowing the cleaner to execute deletions safely.

You typically instantiate this alongside your TestClient inside your test suites:

// modules/user/tests/create.spec.ts
import { TestCleaner } from "@harpiats/common";
import { User, Post } from "app/database";

// Inject the model mapping
const cleaner = new TestCleaner({
  User,
  Post,
});

[!WARNING] If you instantiate the cleaner with an empty object (new TestCleaner({})), attempting to register an ID will throw a "Model not found" error. Always pass the Prisma models you intend to clean.

Model Ordering Matters

Because TestCleaner relies on JavaScript’s Object.keys() to iterate through the model map during the sweep phase, the order in which you declare the models in the constructor matters.

If your schema has strict Foreign Key relationships (e.g., a Post belongs to a User), you must list the child models before their parent models. Otherwise, the cleaner might attempt to delete the parent first, resulting in a database constraint violation error.

// Correct Order: Child (Post) first, Parent (User) second
const cleaner = new TestCleaner({
  Post,
  User,
});

Usage in Tests

The cleaner operates on a simple register-and-sweep mechanism.

1. Registering Records

As you generate mock data inside your tests (preferably using Factories), you must register the created record’s ID to the cleaner.

Use .register(modelName, id) for a single record, or .registerMany(modelName, idsArray) for multiple records.

import { describe, test } from "bun:test";
import { UserFactory } from "app/database/factories/user.factory";

describe("User Profiles", () => {
  test("should fetch a user", async () => {
    // Generate a mock user
    const mockUser = await UserFactory.create();
    
    // Register the user's ID to the cleaner targeting the "User" model map
    cleaner.register("User", mockUser.get("id"));

    // Execute your E2E request...
  });
});

2. Sweeping the Database

To clean the registered records, invoke the clean() method.

Because we want our tests to be perfectly isolated, it is best practice to sweep the database after each test, using bun:test’s afterEach hook.

import { afterEach } from "bun:test";

// Triggers the deletion of all registered IDs and resets the registry array
afterEach(async () => await cleaner.clean());

[!TIP] The .clean() method iterates over all registered models and individually deletes the queued IDs inside a safe block. If a record was already deleted by your application logic during the test, the cleaner silently catches the "Record not found" Prisma exception and safely skips to the next ID.