使用Vitest Mock Kysely的MySQL2数据库实例时遇vi.mock报错
解决Vitest Mock Kysely实例时的"If you are using 'vi.mock' factory, make sure there are no top level variables inside"错误
问题背景
作为测试新手,尝试用Vitest将mysql2数据库Mock为Kysely实例时,触发错误:
If you are using "vi.mock" factory, make sure there are no top level variables inside
涉及代码如下:
database.ts
import * as dotenv from "dotenv"; import { type DB } from "../types/schema.types"; import { createPool, } from "mysql2"; import { Kysely, MysqlDialect } from "kysely"; dotenv.config(); export const dialect = new MysqlDialect({ pool: createPool({ database: `${process.env.DATABASE}`, host: `${process.env.DATABASE_HOST}`, port: process.env.DATABASE_PORT as unknown as number, user: `${process.env.USER}`, password: process.env.PASSWORD, waitForConnections: true, multipleStatements: true, charset: "utf8mb4", connectionLimit: `${process.env.DATABASE_CONNECTION_LIMIT}` as unknown as number, queueLimit: 0, }), }) const db = new Kysely<DB>({ dialect }); export default db;
原测试代码sample.spec.ts
import { it, vi, expect } from "vitest"; import db from "../database/database"; import { DB } from "../types/schema.types"; import { Kysely, MysqlDialect } from "kysely"; // Test case it("Get user count", async () => { const mocks = vi.hoisted(() => ({ query: vi.fn() })); // Mocking database module vi.mock("../database/database", async (importOriginal) => { const actualModule = await importOriginal<typeof import("../database/database")>(); return { ...actualModule, __esModule: true, ownerDb: new Kysely<DB>({ dialect: new MysqlDialect({ pool: vi.fn() }), }), }; }); mocks.query.mockResolvedValue({ rows: [{ count: 1 }] }); const query = db .selectFrom("users") .select((eb) => eb.fn.count<number>("users.user_id").as("count")); const { count } = await db .selectNoFrom((eb) => eb.fn.coalesce(query, eb.lit(0)).as("count")) .executeTakeFirstOrThrow(); expect(count).toBe(1); });
错误原因
- vi.mock的提升机制:Vitest会把
vi.mock提升到模块最顶部执行,此时测试用例内部定义的变量(比如mocks)还未初始化,工厂函数引用这些变量就会触发错误。 - Mock未覆盖目标导出:原
database.ts导出的是默认的db实例,但测试代码里Mock的是ownerDb,完全没覆盖目标对象。 - 未Mock查询方法:即使创建了Mock的Kysely实例,也没有Mock查询链的关键方法,测试时还是会尝试执行真实数据库逻辑。
解决方案
调整后的测试代码
import { it, vi, expect } from "vitest"; import db from "../database/database"; import { DB } from "../types/schema.types"; import { Kysely } from "kysely"; // 用vi.hoisted定义Mock变量,这些变量会被同步提升到模块顶部,和vi.mock同层级 const mocks = vi.hoisted(() => ({ executeTakeFirstOrThrow: vi.fn().mockResolvedValue({ count: 1 }), selectNoFrom: vi.fn().mockReturnThis(), select: vi.fn().mockReturnThis(), selectFrom: vi.fn().mockReturnThis(), })); // 将vi.mock移到模块顶层,避免引用测试用例内的局部变量 vi.mock("../database/database", async () => { return { __esModule: true, // 正确覆盖默认导出的db实例,Mock查询链的核心方法 default: { selectFrom: mocks.selectFrom, select: mocks.select, selectNoFrom: mocks.selectNoFrom, executeTakeFirstOrThrow: mocks.executeTakeFirstOrThrow, } as unknown as Kysely<DB>, }; }); it("Get user count", async () => { const query = db .selectFrom("users") .select((eb) => eb.fn.count<number>("users.user_id").as("count")); const { count } = await db .selectNoFrom((eb) => eb.fn.coalesce(query, eb.lit(0)).as("count")) .executeTakeFirstOrThrow(); expect(count).toBe(1); // 验证查询方法是否按预期被调用 expect(mocks.selectFrom).toHaveBeenCalledWith("users"); });
优化建议
- 使用Kysely官方MockDialect:如果需要兼容更复杂的查询构建逻辑,推荐使用Kysely提供的
MockDialect,它能自动处理查询链的Mock,无需手动Mock每个方法。 - 隔离环境变量:测试时可以Mock
dotenv模块,避免加载真实的.env配置,防止测试环境受生产配置影响。
内容的提问来源于stack exchange,提问作者Almighty Dee
相关产品推荐
相关产品推荐

