Testing (in-memory database)¶
Testing the data layer shouldn't need a running Postgres, temp files or
migrations. createTestDatabase stands up a full tempest-db-js engine over
in-memory SQLite, with the schema created straight from your models — one
connection backs both the DDL and every session, so repositories see the tables
you declared.
It's the port of the tempest-fastapi-sdk testing module, and it's
framework-agnostic (no vitest/jest import) — use it with any harness.
1. A repository test¶
import { BaseRepository, createTestDatabase } from "tempest-express-sdk";
import { UserModel } from "@/db/models/userModel";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
describe("UserRepository", () => {
let db: ReturnType<typeof createTestDatabase>;
beforeEach(() => {
db = createTestDatabase([UserModel]); // creates the `user` table
});
afterEach(() => db.close());
it("creates and fetches a user", async () => {
const repo = new BaseRepository(UserModel, db.session());
const user = await repo.create({
name: "Ana",
email: "ana@x.com",
passwordHash: "...",
});
expect(user.id).toBeTruthy(); // generated UUID
expect(user.isActive).toBe(true); // BaseModel default
expect(await repo.count()).toBe(1);
});
});
createTestDatabase(models) returns { engine, session(), close() }. Pass
db.session() to the repository (or service/controller) exactly as in
production. Every call creates an isolated database — tests don't leak state.
Pass every model the test touches
The models array is what becomes CREATE TABLE. If a repository queries a
related table, include its model too.
2. Automatic scoping with withTestDatabase¶
When you want the database only for a block — and guaranteed close() even if
the body throws — use the wrapper:
import { BaseRepository, withTestDatabase } from "tempest-express-sdk";
import { UserModel } from "@/db/models/userModel";
it("lists empty when there are no rows", async () => {
await withTestDatabase([UserModel], async (db) => {
const repo = new BaseRepository(UserModel, db.session());
expect(await repo.list()).toEqual([]);
});
});
It creates the database, runs the function and close()s in finally —
returning whatever the function returns.
3. Testing a service/controller¶
The whole stack runs on the same db.session():
import { createTestDatabase } from "tempest-express-sdk";
import { UserController } from "@/controllers/userController";
import { UserRepository } from "@/db/repositories/userRepository";
import { UserService } from "@/services/userService";
import { UserModel } from "@/db/models/userModel";
it("maps the row to the response DTO", async () => {
const db = createTestDatabase([UserModel]);
try {
const controller = new UserController(
new UserService(new UserRepository(db.session())),
);
const created = await controller.create({
name: "Ana",
email: "ana@x.com",
passwordHash: "...",
});
expect(created).toMatchObject({ name: "Ana", isActive: true });
} finally {
await db.close();
}
});
Recap¶
createTestDatabase(models)→ in-memory SQLite engine with the models' tables;session()for repositories,close()on teardown.withTestDatabase(models, fn)scopes the database to a block and always closes.- Isolated per call, zero external dependency, harness-agnostic. ✅