如何用Jest模拟ethers.js中Contract.allowance方法?
解决Jest模拟ethers.js Contract的allowance方法失败的问题
问题原因分析
1. spyOn方法失败的原因
ethers.js的Contract实例方法是根据传入的ABI动态生成的,并非预先挂载在Contract.prototype上,所以直接用jest.spyOn(Contract.prototype, 'allowance')会找不到对应属性,报错是必然的。
2. 全局mock Contract类失败的原因
你写的mock代码存在几个问题:
- 错误地将
jest.mock的回调包裹在jest.fn().mockImplementation中,不符合Jest模块mock的语法; - 直接引用
originalEthers会报错,因为jest.mock会被提升到文件最顶部,此时原模块还未完成导入; - 未模拟
BigNumber,如果业务代码中用到了ethers.BigNumber,测试会因找不到该方法而失败。
正确的解决方案
方案一:直接模拟整个ethers模块
这种方式适合不需要用到ethers其他原生功能的场景,直接返回预设的模拟对象:
import approveService from './你的服务文件路径'; // 模拟ethers模块 jest.mock('ethers', () => { // 模拟BigNumber.from方法 const mockBigNumber = { from: jest.fn(val => val) }; // 模拟Contract类,实例化后包含allowance方法 class MockContract { async allowance() { return mockBigNumber.from('100000'); } } return { Contract: MockContract, BigNumber: mockBigNumber }; }); describe('getAllowance', () => { it('正常情况下应返回授权额度', async () => { expect.assertions(1); const response = await approveService.getAllowance( 1116, 'mock-token-address', 'some wallet address', ); expect(response.allowance).toBe('100000'); }); });
方案二:保留ethers原生功能,仅动态模拟Contract实例
这种方式更灵活,适合需要用到ethers其他功能的场景,比如验证BigNumber的处理逻辑:
import { ethers } from 'ethers'; import approveService from './你的服务文件路径'; // 保留ethers原模块的大部分功能,仅替换Contract类为mock jest.mock('ethers', () => { const originalEthers = jest.requireActual('ethers'); return { ...originalEthers, Contract: jest.fn() }; }); describe('getAllowance', () => { it('正常情况下应返回授权额度', async () => { expect.assertions(2); // 模拟allowance方法的返回值 const mockAllowance = jest.fn().mockResolvedValue(ethers.BigNumber.from('100000')); // 让Contract构造函数返回带有allowance方法的实例 ethers.Contract.mockReturnValue({ allowance: mockAllowance }); const response = await approveService.getAllowance( 1116, 'mock-token-address', 'some wallet address', ); // 注意:如果返回的是BigNumber实例,需要转成字符串再断言 expect(response.allowance.toString()).toBe('100000'); // 可选:验证allowance方法是否被正确调用 expect(mockAllowance).toHaveBeenCalledWith('some wallet address', expect.any(String)); }); });
注意事项
- 如果业务代码中最终返回的是
BigNumber实例,断言时需要调用toString()转换为字符串,或者直接用BigNumber实例进行比较; - 测试用例中传入的参数(比如
tokenAddress)要和业务代码的逻辑匹配,若代码不校验地址格式,用任意字符串即可; - 方案二中的
jest.requireActual('ethers')会保留原模块的所有功能,仅替换Contract类,适合复杂场景下的测试。
内容的提问来源于stack exchange,提问作者Lord Loss
相关产品推荐
相关产品推荐

