You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 11:03:20