使用Jest mock Fabric链码ChaincodeStub的报错及方法咨询
Jest 测试 Fabric 链码报错
ctx.stub.putState is not a function 修复方案 问题背景
尝试移除Sinon依赖,仅用Jest为Hyperledger Fabric链码编写单元测试,参考fabric-samples的JS链码测试逻辑编写测试代码后,运行时抛出TypeError: ctx.stub.putState is not a function错误,同时需要了解Jest是否提供类似Sinon createStubInstance的便捷stub创建方法。
原始测试代码如下:
const { ChaincodeStub } = require('fabric-shim'); const MyContract = require('./myContract'); describe('Asset Transfer Basic Tests', () => { let transactionContext; let mockChaincode; let asset; beforeEach(() => { transactionContext = new Context(); mockChaincode = ChaincodeStub; jest.mock('fabric-shim', () => ({ ChaincodeStub: jest.fn().mockImplementation(() => ({ deleteState: jest.fn().mockImplementation(async (key) => { if (mockChaincode.states) { delete mockChaincode.states[key]; } return Promise.resolve(key); }), getState: jest.fn().mockImplementation(async (key) => { let ret; if (mockChaincode.states) { ret = mockChaincode.states[key]; } return Promise.resolve(ret); }), getStateByRange: jest.fn().mockImplementation(async () => { function* internalGetStateByRange() { if (mockChaincode.states) { // Shallow copy const copied = { ...mockChaincode.states }; for (const key in copied) { yield { value: copied[key] }; } } } return Promise.resolve(internalGetStateByRange()); }), putState: jest.fn().mockImplementation((key, value) => { if (!mockChaincode.states) { mockChaincode.states = {}; } mockChaincode.states[key] = value; }), })), })); transactionContext.setChaincodeStub(mockChaincode); asset = { birthDay: '1966-05-31T00:00:00.000Z', firstName: 'Federico', gender: 'male', id: '09c2f565-9923-4b78-bd1c-ff635a70a880', lastName: 'Villegas', }; }); describe('Test InitLedger', (done) => { it('should return error on InitLedger', async () => { mockChaincode.putState.rejects('failed inserting key'); const myContract = new MyContract(); try { await myContract.initLedger(transactionContext); done.fail('initLedger should have failed'); } catch (err) { expect(err.name).toBe('failed inserting key'); } }); it('should return success on InitLedger', async () => { const myContract = new MyContract(); await myContract.initLedger(transactionContext); const ret = JSON.parse( ( await mockChaincode.getState( '09c2f565-9923-4b78-bd1c-ff635a70a880', ) ).toString(), ); expect(ret).toEqual({ ...asset, docType: 'user' }); }); }); });
错误原因
报错由两处核心逻辑错误导致:
jest.mock作用域错误:Jest 会自动将所有jest.mock调用提升到文件顶部执行,写在beforeEach内的 mock 逻辑不会按预期覆盖原始导入,实际拿到的ChaincodeStub是 fabric-shim 导出的原始类,而非自定义的 mock 构造函数。- 传入上下文的对象类型错误:代码直接将
ChaincodeStub构造函数本身传入setChaincodeStub,而非构造函数实例化后的对象。构造函数本身不挂载putState、getState这类实例方法,调用时自然会抛出方法不存在的错误。
额外代码问题:异步测试用例不需要传入done参数,async/await 语法下Jest会自动等待Promise状态变更,传入done反而可能导致测试超时。
修复后代码
将jest.mock移到文件顶部,在beforeEach中实例化新的stub对象传入上下文,每次测试前重置mock状态和模拟存储:
// 顶部定义模块mock,Jest会自动提升该调用覆盖原始模块 jest.mock('fabric-shim', () => { const generateMockStub = () => ({ states: {}, deleteState: jest.fn().mockImplementation(async (key) => { delete mockStub.states[key]; return key; }), getState: jest.fn().mockImplementation(async (key) => { return mockStub.states[key]; }), getStateByRange: jest.fn().mockImplementation(async () => { function* internalGetStateByRange() { const copied = { ...mockStub.states }; for (const key in copied) { yield { value: copied[key] }; } } return internalGetStateByRange(); }), putState: jest.fn().mockImplementation((key, value) => { mockStub.states[key] = value; }), }); return { ChaincodeStub: jest.fn(() => generateMockStub()) }; }); const { ChaincodeStub } = require('fabric-shim'); const MyContract = require('./myContract'); describe('Asset Transfer Basic Tests', () => { let transactionContext; let mockStub; let asset; beforeEach(() => { transactionContext = new Context(); // 每个测试用例生成全新的mock stub,重置状态 mockStub = new ChaincodeStub(); mockStub.states = {}; jest.clearAllMocks(); transactionContext.setChaincodeStub(mockStub); asset = { birthDay: '1966-05-31T00:00:00.000Z', firstName: 'Federico', gender: 'male', id: '09c2f565-9923-4b78-bd1c-ff635a70a880', lastName: 'Villegas', }; }); describe('Test InitLedger', () => { it('should return error on InitLedger', async () => { mockStub.putState.rejects(new Error('failed inserting key')); const myContract = new MyContract(); await expect(myContract.initLedger(transactionContext)).rejects.toThrow('failed inserting key'); }); it('should return success on InitLedger', async () => { const myContract = new MyContract(); await myContract.initLedger(transactionContext); const ret = JSON.parse((await mockStub.getState('09c2f565-9923-4b78-bd1c-ff635a70a880')).toString()); expect(ret).toEqual({ ...asset, docType: 'user' }); }); }); });
Jest 创建 Stub 实例的方法
Jest 没有和 Sinon createStubInstance 完全等价的一键API,但有两个成熟的替代方案:
- 自定义有状态mock实例:通过
jest.mock定义类的构造函数返回值,自行实现需要的方法逻辑、维护内部状态,适合链码状态读写这类需要模拟业务逻辑的场景,也就是上述修复代码使用的写法。 - 自动生成spy实例:如果不需要自定义方法逻辑,只需要一个所有原型方法都被替换为可断言jest函数的空实例,可以直接使用
jest.mocked方法:
这种写法不需要逐个实现类方法,创建后可以直接对方法自定义返回/抛出值、校验调用参数和次数,适合简单逻辑的单元测试。const mockStub = jest.mocked(new ChaincodeStub(), true);
内容的提问来源于stack exchange,提问作者Florian Ldt
相关产品推荐
相关产品推荐

