Merge branch 'parallelize-tests-better'
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -238,3 +238,64 @@ export async function createTestUser(
|
||||
export async function destroyTestDb(db: Kysely<Database>): Promise<void> {
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared Database Singleton (for transaction-based test isolation)
|
||||
// ============================================================================
|
||||
|
||||
let sharedDb: Kysely<Database> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the shared test database once.
|
||||
* Runs migrations and truncates all tables to start with a clean slate.
|
||||
* Subsequent calls return the existing connection.
|
||||
*
|
||||
* Use this with `withTestTransaction()` for fast test isolation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* beforeAll(async () => {
|
||||
* await initTestDb();
|
||||
* });
|
||||
*
|
||||
* test("does something", async () => {
|
||||
* await withTestTransaction(getSharedDb(), async (db) => {
|
||||
* // test code using db
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function initTestDb(): Promise<Kysely<Database>> {
|
||||
if (!sharedDb) {
|
||||
await runMigrations();
|
||||
sharedDb = createTestDb();
|
||||
await truncateAllTables(sharedDb); // Clean slate once at start
|
||||
}
|
||||
return sharedDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared test database connection.
|
||||
* Must call `initTestDb()` first.
|
||||
*
|
||||
* @throws Error if database not initialized
|
||||
*/
|
||||
export function getSharedDb(): Kysely<Database> {
|
||||
if (!sharedDb) {
|
||||
throw new Error(
|
||||
"Test DB not initialized. Call initTestDb() in beforeAll first.",
|
||||
);
|
||||
}
|
||||
return sharedDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the shared test database connection.
|
||||
* Call this in a global afterAll if needed.
|
||||
*/
|
||||
export async function destroySharedDb(): Promise<void> {
|
||||
if (sharedDb) {
|
||||
await sharedDb.destroy();
|
||||
sharedDb = null;
|
||||
}
|
||||
}
|
||||
|
||||
60
apps/api-server/src/__tests__/helpers/test-transaction.ts
Normal file
60
apps/api-server/src/__tests__/helpers/test-transaction.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Transaction-based test isolation helper
|
||||
*
|
||||
* Wraps test code in a transaction that auto-rollbacks, providing
|
||||
* fast test isolation without truncating tables between tests.
|
||||
*/
|
||||
|
||||
import type { Database } from "@reviq/db-schema";
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
/**
|
||||
* Signal used to trigger transaction rollback after test completes
|
||||
*/
|
||||
class RollbackSignal extends Error {
|
||||
constructor() {
|
||||
super("RollbackSignal");
|
||||
this.name = "RollbackSignal";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a test function inside a transaction that auto-rollbacks.
|
||||
*
|
||||
* The transaction implements the same interface as Kysely<Database>,
|
||||
* so it can be passed to context builders and used for all queries.
|
||||
* After the test completes, the transaction is rolled back, providing
|
||||
* instant cleanup without truncating tables.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* test("creates user", async () => {
|
||||
* await withTestTransaction(getSharedDb(), async (db) => {
|
||||
* const user = await createTestUser(db, { email: "test@example.com" });
|
||||
* const ctx = createAPIContext({ db });
|
||||
* // ... test code
|
||||
* }); // Auto-rollback here
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function withTestTransaction<T>(
|
||||
db: Kysely<Database>,
|
||||
testFn: (trx: Kysely<Database>) => Promise<T>,
|
||||
): Promise<T | undefined> {
|
||||
let result: T | undefined;
|
||||
|
||||
try {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
result = await testFn(trx);
|
||||
// Force rollback by throwing after test completes successfully
|
||||
throw new RollbackSignal();
|
||||
});
|
||||
} catch (e) {
|
||||
// Swallow the rollback signal - this is expected behavior
|
||||
if (!(e instanceof RollbackSignal)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user