Search code examples
javascriptunit-testingjestjshyperledger-fabric

Hyperledger Fabric chaincode mocking using Jest


I am trying to add unit tests to my chaincode using Jest. From the sample repo here, it is using Sinon to handle the mocking of ChaincodeStub using createStubInstance. I am looking to remove Sinon dependency and handle the mocking part using Jest.

So far I have been trying:

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' });
        });
    });
});

but so far what I am getting is the following error: TypeError: ctx.stub.putState is not a function.

Might be missing something there.

Is there also something simpler like the createStubInstance provided by Sinon in Jest?


Solution

  • After looking into the link sent by @TSN in the comment, I solved it using the following approach:

    jest.doMock('fabric-shim');
    
    const { ChaincodeStub } = require('fabric-shim');
    
    const MyContract = require('./myContract');
    
    describe('Asset Transfer Basic Tests', () => {
        let transactionContext;
        let chaincodeStub;
        let asset;
    
        beforeEach(() => {
            transactionContext = new Context();
    
            chaincodeStub = new ChaincodeStub();
            transactionContext.setChaincodeStub(chaincodeStub);
    
            chaincodeStub.putState = jest.fn((key, value) => {
                if (!chaincodeStub.states) {
                    chaincodeStub.states = {};
                }
                chaincodeStub.states[key] = value;
            });
    
            chaincodeStub.getState = jest.fn(async (key) => {
                let ret;
                if (chaincodeStub.states) {
                    ret = chaincodeStub.states[key];
                }
                return Promise.resolve(ret);
            });
    
            chaincodeStub.deleteState = jest.fn(async (key) => {
                if (chaincodeStub.states) {
                    delete chaincodeStub.states[key];
                }
                return Promise.resolve(key);
            });
    
            chaincodeStub.getStateByRange = jest.fn(async () => {
                function* internalGetStateByRange() {
                    if (chaincodeStub.states) {
                        // Shallow copy
                        const copied = { ...chaincodeStub.states };
    
                        for (const key in copied) {
                            yield { value: copied[key] };
                        }
                    }
                }
    
                return Promise.resolve(internalGetStateByRange());
            });
    
            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 () => {
                chaincodeStub.putState = () =>
                    Promise.reject(new Error('failed inserting key'));
                const myContract = new MyContract();
                try {
                    await myContract.initLedger(transactionContext);
                    done.fail('initLedger should have failed');
                } catch (err) {
                    expect(err.message).toBe('failed inserting key');
                }
            });
    
            it('should return success on initLedger', async () => {
                const myContract = new MyContract();
                await myContract.initLedger(transactionContext);
                const ret = JSON.parse(
                    (
                        await chaincodeStub.getState(
                            '09c2f565-9923-4b78-bd1c-ff635a70a880',
                        )
                    ).toString(),
                );
                expect(ret).toEqual({ ...asset, docType: 'user' });
            });
        });
    });