如何在Jest中测试作为参数传入的函数是否被调用
我有一个接收函数作为参数的函数,想要验证这个参数函数是否被正确调用。
待测试函数
const loadNamespaces = (setNamespaces) => { namespaceAPI.getNamespaces().then(namespaces => { setNamespaces(namespaces); }); }
我的核心需求是断言mockSetNamespaces已被调用。我能通过jest.spyOn模拟并断言namespaceAPI.getNamespaces被调用,但没法用同样方式断言mockSetNamespaces被调用:
测试代码(首次尝试)
test("loadNamespaces", () => { const mockSetNamespaces = jest.fn(); const mockNamespaces = [ { endpoint: "mock namespace 1", rooms: [] }, ]; jest.spyOn(namespaceAPI, "getNamespaces").mockImplementation(() => { return new Promise((resolve) => { resolve(mockNamespaces); }); }); SocketIOActions.loadNamespaces(mockSetNamespaces); expect(namespaceAPI.getNamespaces).toHaveBeenCalled(); expect(mockSetNamespaces).toHaveBeenCalled(); });
Jest返回的错误信息
● loadNamespaces expect(jest.fn()).toHaveBeenCalled() Expected number of calls: >= 1 Received number of calls: 0
我还试过把setNamespaces放进对象里,想用jest.spyOn方法,但还是没法断言该方法被调用:
测试代码(第二次尝试)
test("loadNamespaces", () => { const mockObject = { mockSetNamespaces: jest.fn(), }; const mockNamespaces = [ { endpoint: "mock namespace 1", rooms: [] }, ]; jest.spyOn(namespaceAPI, "getNamespaces").mockImplementation(() => { return new Promise((resolve) => { resolve(mockNamespaces); }); }); jest.spyOn(mockObject, "mockSetNamespaces").mockImplementation(() => { console.log("Hello from spy function"); }); SocketIOActions.loadNamespaces(mockObject.mockSetNamespaces); expect(namespaceAPI.getNamespaces).toHaveBeenCalled(); expect(mockObject.mockSetNamespaces).toHaveBeenCalled(); });
有日志显示mock函数实际已经被调用:
console.log Hello from spy function
这是Jest的预期行为吗?有没有更简洁的实现方式?
这是Jest的正常行为,问题出在测试代码没有等待异步操作完成就执行了断言。loadNamespaces里的namespaceAPI.getNamespaces()返回Promise,then回调里的setNamespaces调用是异步执行的,而你的测试代码是同步执行断言,此时Promise还未resolve,setNamespaces自然还没被调用。
下面是三种可行的解决方法,按需选择:
方法1:使用async/await
将测试函数改为异步函数,等待异步操作完成后再执行断言。可以直接等待mock的getNamespaces返回的Promise,或用mockResolvedValue简化Promise创建:
test("loadNamespaces", async () => { const mockSetNamespaces = jest.fn(); const mockNamespaces = [{ endpoint: "mock namespace 1", rooms: [] }]; // 用mockResolvedValue简化Promise返回 const mockGetNamespaces = jest.spyOn(namespaceAPI, "getNamespaces") .mockResolvedValue(mockNamespaces); SocketIOActions.loadNamespaces(mockSetNamespaces); await mockGetNamespaces(); // 等待Promise完成 expect(namespaceAPI.getNamespaces).toHaveBeenCalled(); // 还能断言传入的参数是否正确 expect(mockSetNamespaces).toHaveBeenCalledWith(mockNamespaces); });
方法2:使用Jest的done回调
通过测试参数done标记测试完成时机,在setNamespaces的mock回调里调用done:
test("loadNamespaces", (done) => { const mockSetNamespaces = jest.fn((namespaces) => { expect(namespaces).toEqual([{ endpoint: "mock namespace 1", rooms: [] }]); done(); // 调用done表示异步操作完成,测试结束 }); jest.spyOn(namespaceAPI, "getNamespaces") .mockResolvedValue([{ endpoint: "mock namespace 1", rooms: [] }]); SocketIOActions.loadNamespaces(mockSetNamespaces); expect(namespaceAPI.getNamespaces).toHaveBeenCalled(); });
方法3:手动触发所有Promise
用jest.runAllPromises()强制执行所有待处理的异步Promise,再执行断言:
test("loadNamespaces", () => { const mockSetNamespaces = jest.fn(); const mockNamespaces = [{ endpoint: "mock namespace 1", rooms: [] }]; jest.spyOn(namespaceAPI, "getNamespaces").mockResolvedValue(mockNamespaces); SocketIOActions.loadNamespaces(mockSetNamespaces); jest.runAllPromises(); // 触发所有待处理的Promise expect(namespaceAPI.getNamespaces).toHaveBeenCalled(); expect(mockSetNamespaces).toHaveBeenCalled(); });
简化优化点
用mockResolvedValue代替手动创建Promise,能大幅简化mock代码,避免冗余的Promise构造逻辑。
内容的提问来源于stack exchange,提问作者Gustavo Motta

