单元测试中数据库访问代码的测试经常面临同一个问题:测试是否真的反映了 PostgreSQL 的行为?MikroORM 虽然支持 SQLite 等内存驱动,但一旦实体使用了 PostgreSQL 专属的 JSONB、数组类型或 RETURNING 子句,内存库的测试结果就可能失真。要让测试稳定可重复,必须从连接策略、数据清理和 Schema 初始化三个层面同时处理。

真实实例与临时 Schema 的路线选择
首先需要决定测试跑在什么数据库上。不少人会优先选择内存数据库,因为启动快、无外部依赖。但 MikroORM 的 PostgreSQL 驱动在类型映射、查询语法和事务行为上与 SQLite 有明显差异。例如实体中使用了 @Property({ type: 'jsonb' }) 或数组字段,SQLite 会退化为文本或 JSON 字符串,测试断言就会偏离生产环境。因此对于核心仓储逻辑,建议直接连真实 PostgreSQL。
真实实例并不代表所有测试共享一个数据库。临时 Schema 是比较稳妥的做法:每个测试文件或测试套件在 PostgreSQL 中创建独立 Schema,测试结束后删除。MikroORM 的 SchemaGenerator 可以完成建表和索引创建,也可以通过 orm.schema.refreshDatabase() 一键重建。临时 Schema 的好处是隔离彻底,即使测试进程崩溃,后续也可以清理残留。缺点是建表开销略高,但在本地开发机和 CI 中通常可以接受。
import { MikroORM } from '@mikro-orm/postgresql';
import { User } from './user.entity';
let orm: MikroORM;
beforeAll(async () => {
orm = await MikroORM.init({
entities: [User],
dbName: 'app_test',
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'postgres',
schema: 'test_schema_001',
});
await orm.schema.refreshDatabase();
});
afterAll(async () => {
await orm.schema.dropSchema();
await orm.close(true);
});
上面代码中 dbName 指定数据库,schema 指定了当前测试使用的 PostgreSQL Schema。如果测试并发运行,每个测试文件需要使用不同的 Schema 名称,否则会因为重复建表而报错。可以通过环境变量给每个 worker 分配不同的 schema,比如 TEST_SCHEMA=test_schema_${process.pid}。
事务回滚方案:让每个用例拥有干净的事务
临时 Schema 解决了表结构隔离,但同一个测试文件内的多个用例仍然会互相写入数据。例如 createUser 用例创建了一条记录,后续 findByName 用例如果依赖空表,就会受到污染。基于事务回滚的方案可以让每个测试在事务中执行,测试结束后直接回滚,数据库不会留下任何痕迹。
在 Jest 或 Vitest 中,可以在 beforeEach 中开启事务,在 afterEach 中回滚。MikroORM 的 EntityManager 提供了 begin()、commit() 和 rollback() 方法。为了避免测试间共享同一 EntityManager 的持久化上下文,推荐使用 orm.em.fork() 创建独立的 EntityManager,并在每个测试中使用。fork() 会复制一份新的上下文,不会自动提交上一个事务。
下面是一个完整的测试骨架。每个用例拿到的都是干净的 em,写入的数据在 rollback 后不会影响其他用例。
import { MikroORM } from '@mikro-orm/postgresql';
import { EntityManager } from '@mikro-orm/core';
import { User } from './user.entity';
let orm: MikroORM;
let em: EntityManager;
beforeAll(async () => {
orm = await MikroORM.init({
entities: [User],
dbName: 'app_test',
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'postgres',
});
await orm.schema.refreshDatabase();
});
beforeEach(async () => {
em = orm.em.fork();
await em.begin();
});
afterEach(async () => {
await em.rollback();
});
afterAll(async () => {
await orm.close(true);
});
test('创建用户后能立即查询到', async () => {
const user = em.create(User, { name: 'Ada', email: 'ada@ipipp.com' });
await em.flush();
const found = await em.findOne(User, { name: 'Ada' });
expect(found?.email).toBe('ada@ipipp.com');
});
这段代码里的 begin() 会启动一个 PostgreSQL 事务。测试中的 flush() 只是在事务内写入了数据,rollback() 后这些数据不会持久化。需要注意的是,如果测试代码内部调用了 em.commit() 或者通过 orm.em.transactional() 提交了事务,那么外层回滚将无法撤销已经提交的数据。因此建议在测试环境中统一使用事务回滚模式,并避免在仓储方法内手动提交。
另外,PostgreSQL 的 DDL 语句会隐式提交事务,所以使用事务回滚时不能在用例中执行建表、改表等操作。如果需要测试 Schema 变更行为,应该放到独立的 Schema 测试套件中,而不是混在常规仓储测试里。
结合测试容器与 SchemaGenerator 处理生命周期
如果团队希望完全隔离开发环境,可以使用 Testcontainers 在测试启动时拉起一个临时的 PostgreSQL 容器。这样每个开发者或 CI worker 都拥有独立的数据库实例,端口随机分配,不会影响本地已有的 PostgreSQL 服务。MikroORM 只需要读取容器映射后的连接参数即可。
在 Jest 的 globalSetup 中启动容器,在 globalTeardown 中停止容器,可以保证整条测试链路只有一次数据库启动开销。测试文件里则通过环境变量获取连接信息,并交给 MikroORM 初始化。MikroORM 的 SchemaGenerator 支持 updateSchema()、refreshDatabase() 等方法,能够根据实体元数据自动同步表结构。迁移文件场景下也可以使用 migrator.up() 来模拟线上升级路径。
下面是一个使用 testcontainers 的初始化示例。容器启动后,通过 orm.schema.refreshDatabase() 重建所有表。
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { MikroORM } from '@mikro-orm/postgresql';
export default async function globalSetup() {
const container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('test_db')
.withUsername('test_user')
.withPassword('test_pass')
.start();
process.env.TEST_DB_HOST = container.getHost();
process.env.TEST_DB_PORT = String(container.getPort());
process.env.TEST_DB_USER = 'test_user';
process.env.TEST_DB_PASSWORD = 'test_pass';
process.env.TEST_DB_NAME = 'test_db';
(globalThis as any).__POSTGRES_CONTAINER__ = container;
}
测试文件初始化 MikroORM 时读取这些环境变量即可。如果项目使用多个测试文件并发执行,建议在 globalSetup 中完成所有表结构初始化,而不是在每个测试文件里重复调用 refreshDatabase()。否则并发测试可能在同一时间删除或重建表,导致连接中断或数据丢失。
对于已有迁移历史的项目,更推荐使用 orm.getMigrator().up() 来应用迁移文件。这样测试环境与生产环境的 Schema 变更路径完全一致,能提前暴露迁移脚本中的问题。如果只是快速验证实体映射,refreshDatabase() 会更直接。
综合来看,PostgreSQL 与 MikroORM 的单元测试并不是简单用内存库替换就能做好。真实数据库与临时 Schema 保证了类型和行为一致性,事务回滚让每个用例互不干扰,测试容器则把环境依赖降到最低。三者配合之后,仓储层测试才能真正稳定地覆盖 ORM 映射、查询构建和事务边界。
PostgreSQLMikroORM单元测试修改时间:2026-09-30 22:34:31