perf(test-utils): stop re-deleting cleaned rows — up to 5.4× faster (#10762)

Co-authored-by: starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com>
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
starslingdev[bot]
2026-09-03 18:24:29 +00:00
committed by GitHub
co-authored by starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com> Taesu
parent b95b7b31e7
commit b7aeed84ce
7 changed files with 144 additions and 31 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@better-auth/test-utils": patch
---
Adapter test suites now run significantly faster. Cleanup between tests no longer re-deletes rows it has already removed, so suites spend far less time waiting on redundant database round trips.
+1 -1
View File
@@ -45,7 +45,7 @@ If you're using another runtime, keep your existing entry point and add the same
<Callout type="info">
The resulting Hono route must match your Better Auth `basePath`, which
defaults to `/api/auth`. If your app already uses `new
Hono().basePath("/api")`, mount Better Auth at `/auth/*`:
Hono().basePath("/api")`, mount Better Auth at `/auth/*`:
```ts
const app = new Hono().basePath("/api");
+2 -1
View File
@@ -22,7 +22,8 @@
"sideEffects": false,
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch"
"dev": "tsdown --watch",
"test": "vitest"
},
"files": [
"dist"
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import type { CleanupResult } from "./cleanup";
import { cleanupRows } from "./cleanup";
type TestRow = {
id: string;
};
describe("cleanupRows", () => {
it("does not retry rows that were deleted successfully", async () => {
const deletedRowIds: string[] = [];
let trackedRows: Record<string, TestRow[]> = {
user: [{ id: "user-1" }, { id: "user-2" }],
};
const cleanupRow = async (
_model: string,
row: TestRow,
): Promise<CleanupResult> => {
deletedRowIds.push(row.id);
return "complete";
};
trackedRows = await cleanupRows(trackedRows, cleanupRow);
trackedRows = await cleanupRows(trackedRows, cleanupRow);
expect(deletedRowIds).toEqual(["user-1", "user-2"]);
expect(trackedRows).toEqual({});
});
it("retries only rows whose deletion failed", async () => {
const attemptedRowIds: string[] = [];
let secondRowFails = true;
let trackedRows: Record<string, TestRow[]> = {
user: [{ id: "user-1" }, { id: "user-2" }, { id: "user-3" }],
};
const cleanupRow = async (
_model: string,
row: TestRow,
): Promise<CleanupResult> => {
attemptedRowIds.push(row.id);
if (row.id === "user-2" && secondRowFails) {
secondRowFails = false;
throw new Error("Temporary delete failure");
}
return "complete";
};
trackedRows = await cleanupRows(trackedRows, cleanupRow);
expect(trackedRows).toEqual({ user: [{ id: "user-2" }] });
trackedRows = await cleanupRows(trackedRows, cleanupRow);
expect(attemptedRowIds).toEqual(["user-1", "user-2", "user-3", "user-2"]);
expect(trackedRows).toEqual({});
});
it("retains rows explicitly marked for retry", async () => {
let modelAvailable = false;
let trackedRows: Record<string, TestRow[]> = {
customModel: [{ id: "custom-1" }],
};
const cleanupRow = async (): Promise<CleanupResult> =>
modelAvailable ? "complete" : "retry";
trackedRows = await cleanupRows(trackedRows, cleanupRow);
expect(trackedRows).toEqual({ customModel: [{ id: "custom-1" }] });
modelAvailable = true;
trackedRows = await cleanupRows(trackedRows, cleanupRow);
expect(trackedRows).toEqual({});
});
});
@@ -0,0 +1,29 @@
export type RowsByModel<Row> = Record<string, Row[]>;
export type CleanupResult = "complete" | "retry";
export async function cleanupRows<Row>(
trackedRows: Readonly<Record<string, readonly Row[]>>,
cleanupRow: (model: string, row: Row) => Promise<CleanupResult>,
): Promise<RowsByModel<Row>> {
const rowsToRetry: RowsByModel<Row> = {};
for (const [model, rows] of Object.entries(trackedRows)) {
const failedRows: Row[] = [];
for (const row of rows) {
try {
const result = await cleanupRow(model, row);
if (result === "retry") {
failedRows.push(row);
}
} catch {
failedRows.push(row);
}
}
if (failedRows.length > 0) {
rowsToRetry[model] = failedRows;
}
}
return rowsToRetry;
}
@@ -6,7 +6,7 @@ import type {
Verification,
} from "@better-auth/core/db";
import { createLocalAccountIssuer, getAuthTables } from "@better-auth/core/db";
import type { DBAdapter } from "@better-auth/core/db/adapter";
import type { DBAdapter, Where } from "@better-auth/core/db/adapter";
import {
createAdapterFactory,
deepmerge,
@@ -17,6 +17,8 @@ import { generateId } from "@better-auth/core/utils/id";
import type { Auth } from "better-auth";
import { betterAuth } from "better-auth";
import { test } from "vitest";
import type { RowsByModel } from "./cleanup";
import { cleanupRows } from "./cleanup";
import type { Logger } from "./test-adapter";
/**
@@ -122,6 +124,10 @@ type Failure<E> = {
type Result<T, E = Error> = Success<T> | Failure<E>;
type CreatedRow = {
id: Where["value"];
};
async function tryCatch<T, E = Error>(
promise: Promise<T>,
): Promise<Result<T, E>> {
@@ -251,7 +257,7 @@ export const createTestSuite = <
customIdGenerator?: () => any | Promise<any> | undefined;
transformIdOutput?: (id: any) => string | undefined;
}) => {
const createdRows: Record<string, any[]> = {};
let createdRows: RowsByModel<CreatedRow> = {};
let adapter = await helpers.adapter();
const wrapperAdapter = (
@@ -352,7 +358,7 @@ export const createTestSuite = <
select,
forceAllowId: true,
});
createdRows[model] = [...(createdRows[model] || []), res];
(createdRows[model] ??= []).push({ id: res.id });
return res as any;
},
options: adapter.options,
@@ -374,34 +380,28 @@ export const createTestSuite = <
};
const cleanupCreatedRows = async () => {
if (Object.keys(createdRows).length === 0) return;
adapter = await helpers.adapter();
for (const model of Object.keys(createdRows)) {
for (const row of createdRows[model]!) {
const schema = getAuthTables(helpers.getBetterAuthOptions());
const getDefaultModelName = initGetDefaultModelName({
schema,
usePlural: adapter.options?.adapterConfig.usePlural,
});
let defaultModelName: string;
try {
defaultModelName = getDefaultModelName(model);
} catch {
continue;
}
if (!schema[defaultModelName]) continue; // model doesn't exist in the schema anymore, so we skip it
try {
await adapter.delete({
model,
where: [{ field: "id", value: row.id }],
});
} catch {
// We ignore any failed attempts to delete the created rows.
}
if (createdRows[model]!.length === 1) {
delete createdRows[model];
}
const schema = getAuthTables(helpers.getBetterAuthOptions());
const getDefaultModelName = initGetDefaultModelName({
schema,
usePlural: adapter.options?.adapterConfig.usePlural,
});
createdRows = await cleanupRows(createdRows, async (model, row) => {
let defaultModelName: string;
try {
defaultModelName = getDefaultModelName(model);
} catch {
return "retry";
}
}
if (!schema[defaultModelName]) return "retry";
await adapter.delete({
model,
where: [{ field: "id", value: row.id }],
});
return "complete";
});
};
// Track current applied BetterAuth options state
+3
View File
@@ -0,0 +1,3 @@
import { defineProject } from "vitest/config";
export default defineProject({});