15

So I have been struggling with how to test dynamic imports generally and in this case, especially in jest, I have been reading a lot on the internet but did not find anything concrete, so I thought about bringing the question up to centralize a decent solution.

I have the following methods inside of a class

class MyClass {
   successMethod() {  ...  }

   errorMethod() { ... }

    myMethod() {
        return import('./myFile.js')
           .then(() => this.successMethod())
           .catch(() => this.errorMethod());
    }
}

My question is:

How do you mock both Success and Failing promise cases for this dynamic import using Jest to make sure each method (successMethod and errorMethod) are called when resolving or failing respectively?.

I found jest.doMock helps for mocking the resolved case but did not find a way to make the dynamic import fail by mocking it so the catch case is uncovered.

Note: this is not a react application, this is a Vanilla JS project.

Enmanuel Duran
  • 4,988
  • 3
  • 17
  • 29
  • 1
    what about `jest.mock('./myFile.js', () => Promise.reject(new Error('Forcing async import error')));`? – Dipen Shah Sep 17 '20 at 16:55
  • @DipenShah This will result in successful import with `default` export being promise object. – Estus Flask Sep 22 '20 at 17:51
  • @EstusFlask yes and that should help you test fail scenario, right? Could you please share codesandbox/stablitz project, is should work afaik. – Dipen Shah Sep 22 '20 at 17:52
  • @DipenShah jest.mock return is treated as CommonJS export which is translated to ESM `default` export. It will trigger `then` callback and not `catch`, because `import` returns a promise of ES module export object, which in this case is `{ default: Promise.reject(...) }`. – Estus Flask Sep 22 '20 at 17:56

2 Answers2

9

Prototype methods can be spied or mocked on either MyClass.prototype or class instance. Module mocks and spy functions should be restored for each test in order for them to not affect each other.

let myClass;
beforeEach(() => {
  jest.resetModules();
  jest.restoreAllMocks();
  myClass = new MyClass();
  jest.spyOn(myClass, 'successMethod');
  jest.spyOn(myClass, 'errorMethod');
});

jest.doMock requires to import all affected modules after it was called. In order for dynamic import to result in rejected promise, myFile module should throw an error when evaluated. Since dynamic import returns a promise, tests should be asynchronous.

it('...', async () => {
  jest.mock('./myFile.js', () => 'value');
  await expect(myClass.myMethod()).resolves.toEqual(/* return value from successMethod */);
  expect(myClass.successMethod).toBeCalled();
  expect(myClass.errorMethod).not.toBeCalled();
});

it('...', async () => {
  jest.mock('./myFile.js', () => { throw new Error() });
  await expect(myClass.myMethod()).rejects.toThrow(/* error message from errorMethod */);
  expect(myClass.successMethod).not.toBeCalled();
  expect(myClass.errorMethod).toBeCalled();
});
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • I was already doing this but it wasn't working, now I found out that it is because I wasn't using `jest.resetModules();` and `jest.restoreAllMocks();` just to accept the answer could you change the error case to `.rejects` instead of `.resolves`. – Enmanuel Duran Sep 23 '20 at 21:25
  • It should be `resolves` instead of `rejects` because of `catch` - unless it's errorMethod that causes a rejection. – Estus Flask Sep 23 '20 at 21:27
  • Yes in my case the errot method causes a rejection. – Enmanuel Duran Sep 25 '20 at 00:44
  • You can remove the comment about the discrepancies of the_ in the methods, I fixed it to make it easier to understand. – Enmanuel Duran Sep 25 '20 at 00:50
0

maybe something like

beforeEach(() => {
  jest.resetModules();
});
beforeAll(() => {
  MyClass.mockImplementation(() => {
    return {
      successMethod: () => {
        console.log("succees");
      },
      errorMethod: () => {
        console.log("error");
      }
    };
  });
});

test("should fail", () => {
  jest.doMock("./myFile.js", () => {
    return jest.fn(() => {
      throw new Error("Parameter is not a number!");
    });
  });
  const kls = MyClass();
  kls.myMethod();
  expect(kls.errorMethod).toHaveBeenCalledTimes(1);
  expect(kls.successMethod).toHaveBeenCalledTimes(0);
});
test("should pass", () => {
  jest.doMock("./myFile.js", () => {
    return jest.fn(() => 1);
  });
  const kls = MyClass();
  kls.myMethod();
  expect(kls.errorMethod).toHaveBeenCalledTimes(0);
  expect(kls.successMethod).toHaveBeenCalledTimes(1);
});
Itamar
  • 1,601
  • 1
  • 10
  • 23
  • 1
    The chosen approach is right but there are several problems here. MyClass is not a spy and cannot be mocked. Mocked implementation lacks original myMethod and there's nothing to test. myMethod is asynchronous and needs asynchronous tests. – Estus Flask Sep 22 '20 at 18:18