这是一个例子。
有3个测试文件
test
- customer.test.ts
- user.test.ts
- staff.test.ts
Run Code Online (Sandbox Code Playgroud)
它们每个都需要一个数据库连接(无论您使用什么数据库和 ORM),因此您将在所有测试文件中看到以下行。
describe('staff.test.ts', () => {
let testDb;
beforeAll(async () => {
testDb = await getTestDb(); // This returns a DB connection
});
afterEach(async () => {
await testDb.synchronize(true);
});
afterAll(async () => {
await testDb.close();
});
// tests are here
});
Run Code Online (Sandbox Code Playgroud)
在所有的文件中,
testDb总是用相同的函数实例化 ( getTestDb())beforeAll、afterEach、afterAll被复制/粘贴。我怎么能够...
beforeAllandafterAllafterEach每个测试用例?似乎globalSetup允许我导出“在所有测试套件之前触发一次的异步函数”。但是,我不仅必须运行beforeAll,而且还必须共享实例化的数据库连接。
可以实现吗?任何建议将被认真考虑!
小智 0
我最近偶然发现了同样的问题,并制定了自己的框架式解决方案。
Jest 不在全局运行器和套件之间共享对象实例(通过全局对象对可序列化数据的支持有限),因此我们将使用数据库管理服务器在它们之间进行通信(jest-puppeteer使用共享的 ws 端点)。
Jest 配置看起来像这样:
export default {
...,
globalSetup: "globalSetup.ts",
globalTeardown: "globalTeardown.ts",
setupFilesAfterEnv: ["database.setup.ts"],
};
Run Code Online (Sandbox Code Playgroud)
globalSetup.ts
import type { Config } from "@jest/types";
import { handlers, startServer } from "./db-manager";
declare global {
var controlHandle: {
kill: () => Promise<unknown>;
handlers: typeof handlers;
};
var serverConfig: {
port: number;
hostname: string;
};
}
export default async (
config: Config.GlobalConfig,
projectConfig: Config.ProjectConfig,
) => {
const serverConfig: (typeof globalThis)["serverConfig"] = {
// Pick a port to communicate, I look for a free on every run
port: -1,
hostname: "localhost",
};
// This will be serialized and passed to setupFilesAfterEnv files as well as tests themselves
projectConfig.globals.serverConfig = routerConfig;
// This spins up a DB manager server and resolves whenever it's ready
const serverHandle = await startServer(serverConfig.port, serverConfig.hostname);
globalThis.controlHandle = {
kill: serverHandle.kill,
handlers,
};
// My config data has amount of workers Jest is running so I can limit my DB pool to that value
await handlers.setup(config.maxWorkers);
};
Run Code Online (Sandbox Code Playgroud)
globalTeardown.ts
export default async () => {
// Shutting down still running queries
await globalThis.controlHandle.handlers.teardown();
// Killing the server itself
await globalThis.controlHandle.kill();
};
Run Code Online (Sandbox Code Playgroud)
database.setup.ts
import { getDatabase } from "app/database";
const { port, hostname } = globalThis.routerConfig;
// Some kind of http client, implementation omitted
const client = createClient(hostname, port)
declare global {
var testContext:
| {
database: MyDatabaseBuilder;
databaseName: string;
}
| undefined;
}
beforeAll(async () => {
// Wait until server resolves with a free database
const { databaseName, connectionData } = await client.lockDatabase();
global.testContext = {
// Implementation ommited, any SQL builder you prefer
database: getDatabase(connectionData, databaseName),
databaseName,
};
});
afterEach(async () => {
const { databaseName } = global.testContext!;
// Clear up a database before next `test` in a suite
await client.truncateDatabase(databaseName);
});
afterAll(async () => {
const { databaseName, database } = global.testContext!;
// Don't forget to close a Pool / do cleanup on database object
await database.cleanup();
// Report to a DB manager server that this database may be released
await client.releaseDatabase(databaseName);
});
Run Code Online (Sandbox Code Playgroud)
这就是您在测试中使用它的方式:
write.test.ts
test("write to DB", () => {
const { database } = global.testContext!;
// This .sql(..) syntax is a stub
await database.sql("INSERT INTO products (id, name, price) VALUES (1, "Soap", 100)");
expect(await database.sql("SELECT * FROM products")).toHaveLength(1);
console.log("Current database:", await database.sql("SELECT current_database()"))
});
Run Code Online (Sandbox Code Playgroud)
read.test.ts
test("read from DB", () => {
const { database } = global.testContext!;
// This is a different database
expect(await database.sql("SELECT * FROM products")).toHaveLength(0);
console.log("Current database:", await database.sql("SELECT current_database()"))
});
Run Code Online (Sandbox Code Playgroud)
如果您正确地实现了数据库管理器,那么您将在两个套件上拥有不同的数据库,或者在套件之间拥有相同的数据库但被截断。
我使用带有 Postgres 的 docker 镜像作为数据库管理器,它的完成方式得到了极大的简化:
db-manager.ts
import { GenericContainer, StartedTestContainer } from "testcontainers";
import { EventEmitter } from "events";
type ManagerInstance = {
container: StartedTestContainer;
maxDatabases: number;
connectionData: {
host: string;
username: string;
password: string;
port: number
}
}
let instance: ManagerInstance | undefined;
type DatabaseInstance = {
name: string;
locked: boolean;
}
let databases: DatabaseInstance[] = [];
const databaseEventEmitter = new EventEmitter();
const createDatabase = async () => {
if (databases.length >= maxDatabases) {
return;
}
// You might want to implement a queue for a template database
// As it is not possible to clone several DBs from one template concurrently
const name = /* pick a random name */;
const database = getDatabase(connectionData, templateDatabaseName);
await database.sql(`CREATE DATABASE "${name}" TEMPLATE "${templateDatabaseName}"`);
await database.cleanup();
databases.push({ name, locked: false });
databaseEventEmitter.emit("release", name);
}
export const handlers = {
setup: async (maxDatabases: number) => {
const postgresData = {
host: "localhost",
port: -1
user: "postgres",
password: "postgres-password",
templateDatabase: "template-db",
}
const container = new GenericContainer("postgres")
.withExposedPorts(postgresData.port)
.withEnv("POSTGRES_USER", postgresData.user)
.withEnv("POSTGRES_PASSWORD", postgresData.password)
.withEnv("POSTGRES_DB", postgresData.templateDatabase);
const runningContainer = await container.start();
const connectionData = {
host: postgresData.host,
username: postgresData.user,
password: postgresData.password,
port: runningContainer.getMappedPort(postgresData.port),
};
const database = getDatabase(connectionData, postgresData.templateDatabase);
// Probably, you want your databases be at the same state schema-wise before test
// Implementation omitted
await migrate(database);
await database.cleanup();
instance = {
container: runningContainer,
connectionData,
maxDatabases,
};
},
teardown: async () => {
await instance.container.stop({ timeout: 10000 });
},
lockDatabase: async () => {
// First, we try to find a database not locked by other threads
const firstUnlockedDatabase = databases.find((database) => !database.locked);
if (firstUnlockedDatabase) {
firstUnlockedDatabase.locked = true;
return {
databaseName: firstUnlockedDatabase.name,
connectionData: instance.connectionData,
};
}
// If we didn't get to the limit of DBs, we create a new one
if (databases.length < instance.maxDatabases) {
// We don't wait until it's got created, just subscribe to the event
void createDatabase();
}
// Otherwise, we wait until a database will get released
return new Promise((resolve) => {
databaseEventEmitter.once("release", (databaseName) => {
const database = databases.find((database) => database.name === databaseName);
database.locked = true;
resolve({
databaseName: name,
connectionData: instance.connectionData,
})
})
});
},
truncateDatabase: (databaseName: string) => {
const database = getDatabase(instance.connectionData, databaseName);
// We could just drop a DB and recreate it from a template, but it takes way too long
await database.sql("TRUNCATE products RESTART IDENTITY");
// If you get pg-pool errors on Jest exit, probably this cleanup didn't finish before process stopped
// You may want to implement a cleanup manager that will track all database instances needed to close connections before exit
// And run global cleanup on teardown handler
await database.cleanup();
},
releaseDatabase: (databaseName: string) => {
const database = databases.find((database) => database.name === databaseName);
database.locked = false;
databaseEventEmitter.emit("release", databaseName);
},
};
export const startServer = async (port: number) => {
// Implement a server using handlers from above and listening on given port
// Resolves when starts to listen
}
Run Code Online (Sandbox Code Playgroud)
完整的工作版本可以在我在实现它的过程中制作的PR中找到。