如何在jest+supertest集成测试中mock redis createClient的实现?
问题根本原因
你遇到的不是supertest的限制,是模块导入执行顺序导致的:
- 单元测试中你先完成
redis模块mock,再实例化RedisSecretRepository,此时调用createClient用的就是你后续修改的mock返回值 - 集成测试中你先执行
import server from "../../src/server",server模块在导入阶段就已经完成了Redis客户端的初始化(提前调用了createClient创建实例),你在测试用例内修改mockReturnValue的时间点晚于客户端实例的创建时间,自然不会生效。
解决方案
方案1:调整导入顺序,用动态导入加载server
把server的导入放到mock修改完成之后,通过动态导入的方式避免模块预执行:
import supertest from "supertest"; import { createClient } from "redis"; jest.mock("redis"); const mockedCreateClient = createClient as jest.MockedFunction<any>; describe("Get Secrets By Id Integration Tests", () => { beforeEach(() => { // 每次用例执行前重置模块缓存,保证导入的server是全新实例 jest.resetModules(); }); it("should retrieve a secret", async () => { // 先修改mock返回值 const mockedRedisClient = { connect: jest.fn(), get: jest.fn(async () => "123qwe"), on: jest.fn(), }; mockedCreateClient.mockReturnValue(mockedRedisClient); // 再动态导入server,此时server初始化Redis用的就是上面的mock const server = (await import("../../src/server")).default; const request = supertest(server.app); const res = await request.get("/api/v1/secrets/123456qwerty"); expect(res.status).toBe(200); expect(res.body).toEqual({ secret: "123qwe" }); }); it("should return 404 when secret not exists", async () => { // 每个用例都可以自定义mock行为 const mockedRedisClient = { connect: jest.fn(), get: jest.fn(async () => null), on: jest.fn(), }; mockedCreateClient.mockReturnValue(mockedRedisClient); const server = (await import("../../src/server")).default; const request = supertest(server.app); const res = await request.get("/api/v1/secrets/not_exists_id"); expect(res.status).toBe(404); }); });
方案2:将Redis客户端改为懒加载模式
修改业务代码的Redis客户端初始化逻辑,不要在模块导入阶段就创建实例,改成第一次被调用时才初始化,保证测试用例修改mock的时间足够早:
// 原代码(模块导入时就创建客户端) const redisClient = createClient(); export class RedisSecretRepository { constructor() { this.client = redisClient; } } // 修改为懒加载 export class RedisSecretRepository { constructor() { this.client = null; } async getClient() { if (!this.client) { this.client = createClient(); await this.client.connect(); } return this.client; } async getSecretByUrlId(urlId) { const client = await this.getClient(); return client.get(urlId.toString()); } }
方案3:使用共享mock客户端实例(无需修改业务代码)
在mock工厂函数中返回一个可修改的共享mock客户端实例,测试用例直接修改该实例的方法行为即可:
// 定义可修改的共享mock客户端 const mockRedisClient = { connect: jest.fn(), get: jest.fn(), on: jest.fn() }; jest.mock("redis", () => ({ createClient: jest.fn(() => mockRedisClient) })); import supertest from "supertest"; import server from "../../src/server"; const request = supertest(server.app); describe("Get Secrets By Id Integration Tests", () => { beforeEach(() => { // 每个用例前重置所有mock的调用记录和返回值 jest.clearAllMocks(); }); it("should retrieve a secret", async () => { // 直接修改共享实例的get方法返回值 mockRedisClient.get.mockResolvedValue("123qwe"); const res = await request.get("/api/v1/secrets/123456qwerty"); expect(res.status).toBe(200); expect(res.body).toEqual({ secret: "123qwe" }); }); it("should return 404 when secret not found", async () => { mockRedisClient.get.mockResolvedValue(null); const res = await request.get("/api/v1/secrets/invalid_id"); expect(res.status).toBe(404); }); });
内容的提问来源于stack exchange,提问作者JMir
相关产品推荐
相关产品推荐

