Search code examples
typescriptjestjssinonproxyquireadm-zip

Stub adm-zip with proxyquire and sinon in TypeScript


I want to unit test the class zip.adapter.ts with jest. I tried a lot of different methods to mock/stub the adm-zip package but nothing works.

I first tried ts-mock-imports but it always fails if I try to mock adm-zip. Then I tried sinon but it either failed to stub adm-zip or it just didn't stub it. My last resort was to combine sinon with proxyquire but that doesn't seem to work either....

Has someone an idea why this doesn't work? When the test calls the unzip method the code in it still uses the real adm-zip implementation...

(I know the unit test doesn't make much sense because everything is mocked but I'm required to do it because of test coverage rules that I cannot change)

zip.adapter.ts

import * as admZip from 'adm-zip';

export class ZipAdapter {
    constructor() {}

    unzip(zip: Buffer, path: string) {
        const unzip = new admZip(zip);
        unzip.extractAllTo(path, true);
    }
}

zip.adapter.spec.ts

import * as sinon from 'sinon';
import { ZipAdapter } from './zip.adapter';
import * as proxyquire from 'proxyquire';

describe('Zip Adapter', () => {
    let zipAdapter: ZipAdapter;

    beforeEach(() => {
        const admZipInstance = { extractAllTo: sinon.stub() };
        const admZipStub = sinon.stub().callsFake(() => admZipInstance);
        const moduleStub = proxyquire('./zip.adapter.ts', { 'adm-zip': admZipStub });

        zipAdapter = new moduleStub.ZipAdapter();
    });

    it('should be defined', () => {
        expect(zipAdapter).toBeDefined();
    });

    it('should have called extractAllTo', () => {
        zipAdapter.unzip(Buffer.from(''), 'test');
    });
});

Update:

I got my test working with Jest but only if I require() my module. If I use my import without the require() the mock doesn't work. Is it possible to get rid the require() and only use the import?

import { ZipAdapter } from './zip.adapter';

describe('Zip Adapter', () => {
    let zipAdapter: ZipAdapter;
    let admZipExtractAllMock: jest.Mock<any, any>;

    beforeEach(() => {
        const admZipMock = jest.fn();
        admZipExtractAllMock = jest.fn();
        admZipMock.mockImplementation(() => {
            return { extractAllTo: admZipExtractAllMock };
        });
        jest.mock('adm-zip', () => admZipMock);

        const zipAdapterModule = require('./zip.adapter');
        zipAdapter = new zipAdapterModule.ZipAdapter();
    });

    it('should be defined', () => {
        expect(zipAdapter).toBeDefined();
    });

    it('should have called extractAllTo', () => {
        zipAdapter.unzip('unit', 'test');
        expect(admZipExtractAllMock.mock.calls.length).toBe(1);
    });
});

Solution

  • Top-level import only imports ZipAdapter type so it's evaluated the first time when it's imported with require. If it were mocked with jest.mock after the import, this couldn't affect imported module.

    If it needs to be mocked for all tests, it should be mocked and imported at top level:

    import * as zipAdapterModule from './zip.adapter';
    
    jest.mock('adm-zip', () => {
      let admZipExtractAllMock = jest.fn();
      return {
        __esModule: true,
        admZipExtractAllMock,
        default: jest.fn(() => ({ extractAllTo: admZipExtractAllMock }))
    });
    

    jest.mock at top level is hoisted above import. admZipExtractAllMock spy is exposed as named export in order to be able to change the implementation any time, preferable with Once methods to not affect other tests.

    If it needs to not be mocked for some tests or Jest spy API is not enough to change the implementation, it needs to be mocked with jest.mock and imported with require inside a test like shown in the OP. In this case jest.resetModules should be added to allow mocked module to be re-imported.