JCC Express

Testing

Database Testing

Database assertions let you verify that your application correctly reads and writes to the database. They are available on every TestCase instance and query the real database.


Available assertions

TypeScript
// Assert at least one row matches the given columns
await test.assertDatabaseHas("users", { email: "alice@example.com" });

// Assert no row matches
await test.assertDatabaseMissing("users", { email: "deleted@example.com" });

// Assert exact row count
await test.assertDatabaseCount("users", 5);

// Assert a model instance's primary key exists in its table
await test.assertModelExists(user);

// Assert a model instance's primary key is gone from its table
await test.assertModelMissing(user);

Example

TypeScript
import { describe, it, beforeEach, afterEach } from "bun:test";
import { TestCase } from "../TestCase";
import { Factory, Faker } from "../../Testing/Factory";
import { User } from "../../app/Models/User";

const userFactory = Factory.define(User as any, () => ({
  name: Faker.fullName(),
  email: Faker.email(),
  password: "secret",
}));

describe("User database assertions", () => {
  const test = new (class extends TestCase {})();

  beforeEach(async () => await test.setUp());
  afterEach(async () => await test.tearDown());

  it("inserts and finds a record", async () => {
    await test.withTransaction(async () => {
      const user = await userFactory.create();

      await test.assertDatabaseHas("users", { email: user.email });
      await test.assertModelExists(user);
      await test.assertDatabaseCount("users", 1);
    });
  });

  it("removes and confirms deletion", async () => {
    await test.withTransaction(async () => {
      const user = await userFactory.create();
      await user.delete();

      await test.assertDatabaseMissing("users", { id: user.id });
      await test.assertModelMissing(user);
    });
  });
});

How withTransaction keeps the database clean

withTransaction wraps your callback in a Knex transaction and always rolls it back when the callback finishes — even if it throws. Your test data never survives to the next test.

TypeScript
await test.withTransaction(async () => {
  // everything in here sees the same transaction
  await User.create({ name: "Alice", email: "a@test.com" });
  await test.assertDatabaseHas("users", { name: "Alice" });
  // ← transaction is rolled back here; the row disappears
});

The rollback is automatic and happens whether the test passes or throws. You do not need to clean up manually.

Under the hood the framework uses AsyncLocalStorage (Builder.txStorage) to propagate the Knex trx object to every database query made inside the callback — including queries inside your controllers and models — without you having to pass anything around.


How model assertions work

assertModelExists(model) and assertModelMissing(model) use the model's table name and primary key:

TypeScript
// Equivalent assertions — pick whichever reads better
await test.assertDatabaseHas("users", { id: user.id });
await test.assertModelExists(user);

The model must have getTable() and getKeyName() / getKey() methods (provided by the JCC Eloquent base model).


Notes

  • All assertions throw on failure with a descriptive message
  • assertDatabaseHas matches rows where ALL provided columns match (an AND query)
  • Make sure your .env test database is configured before running tests; assertions run against the real database