1

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);
    });
});
Shamshiel
  • 2,051
  • 3
  • 31
  • 50
  • Why do you use Sinon and Proxyquire with Jest? It's supposed to replace both. – Estus Flask Nov 04 '20 at 08:58
  • There is no real reason why I didn't just use Jest for mocking. I just had more experience using Sinon and until now I never had a problem with it. I changed my test to use Jest and I got it working with it but I don't really understand why I have to use the require() like this. – Shamshiel Nov 04 '20 at 10:21
  • Jest integration of function and module mocks is tight enough so it's easier to use its own features.I expect Proxyquire to be incompatible with Jest because both mess with Node module API in similar ways. There's also a problem in production code, `import * as admZip from` means that TS module interop wasn't configured properly because `*` import cannot be a function by specs. CommonJS module should be a default import, `import admZip from` This means that the mock suggested in the answer may not work for you without a fix, see https://www.staging-typescript.org/tsconfig#esModuleInterop – Estus Flask Nov 04 '20 at 16:53

1 Answers1

2

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.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565