I've attempted to follow the ES6 Class Mocks page in the Jest documentation to test a method on a TypeScript class Consumer
. This class instantiates a Provider
object and calls methods on it, so I would like to mock the Provider
class.
Directory structure:
.
├── __tests__
│ └── consumers
│ └── Consumer.test.ts
└── js
├── providers
│ └── provider.ts
└── consumers
└── Consumer.ts
provider.ts
:
export class Provider {
constructor() {}
public action(params) {
// do some stuff that we need to mock
return something;
}
}
Consumer.ts
:
import {Provider} from "../providers/provider";
export class Consumer {
private provider: Provider;
constructor() {
this.provider = new Provider();
}
public doSomething() {
const result = this.provider.action(params);
// do something with 'result'
}
}
My first attempt was with a default "automatic mock":
Consumer.test.ts
:
import {Consumer} from "../../js/consumers/Consumer";
jest.mock("../../js/providers/provider");
test("Consumer doSomething", () => {
// a mock Provider will be instantiated in Consumer's ctor:
const consumer = new Consumer();
// however, Provider.action() will return undefined within doSomething()
consumer.doSomething();
});
This proves that I can replace the real implementation with a mock, but I need to ensure that Provider.action()
returns a value, so next I tried:
// at some point we can make this return something, but first check it works
const mockAction = jest.fn();
jest.mock("../../js/providers/provider", () => {
return jest.fn().mockImplementation(() => {
return {action: mockAction};
});
});
test("Consumer doSomething", () => {
// throws TypeError: provider_1.Provider is not a constructor
const consumer = new Consumer();
consumer.doSomething();
});
No matter how I've tried changing the mock, I can't find a solution which allows me to use Consumer
as normal from my test. I'd rather avoid creating "manual mocks" so I can keep the codebase cleaner and vary the mock implementation between tests.
I've attempted to follow the ES6 Class Mocks page in the Jest documentation to test a method on a TypeScript class Consumer
. This class instantiates a Provider
object and calls methods on it, so I would like to mock the Provider
class.
Directory structure:
.
├── __tests__
│ └── consumers
│ └── Consumer.test.ts
└── js
├── providers
│ └── provider.ts
└── consumers
└── Consumer.ts
provider.ts
:
export class Provider {
constructor() {}
public action(params) {
// do some stuff that we need to mock
return something;
}
}
Consumer.ts
:
import {Provider} from "../providers/provider";
export class Consumer {
private provider: Provider;
constructor() {
this.provider = new Provider();
}
public doSomething() {
const result = this.provider.action(params);
// do something with 'result'
}
}
My first attempt was with a default "automatic mock":
Consumer.test.ts
:
import {Consumer} from "../../js/consumers/Consumer";
jest.mock("../../js/providers/provider");
test("Consumer doSomething", () => {
// a mock Provider will be instantiated in Consumer's ctor:
const consumer = new Consumer();
// however, Provider.action() will return undefined within doSomething()
consumer.doSomething();
});
This proves that I can replace the real implementation with a mock, but I need to ensure that Provider.action()
returns a value, so next I tried:
// at some point we can make this return something, but first check it works
const mockAction = jest.fn();
jest.mock("../../js/providers/provider", () => {
return jest.fn().mockImplementation(() => {
return {action: mockAction};
});
});
test("Consumer doSomething", () => {
// throws TypeError: provider_1.Provider is not a constructor
const consumer = new Consumer();
consumer.doSomething();
});
No matter how I've tried changing the mock, I can't find a solution which allows me to use Consumer
as normal from my test. I'd rather avoid creating "manual mocks" so I can keep the codebase cleaner and vary the mock implementation between tests.
3 Answers
Reset to default 2You don't have to use a default export here. When using named exports you need to create a mock that matches the "shape" of your module. So in your case:
const mockAction = jest.fn();
jest.mock("../../js/providers/provider", () => ({
Provider: jest.fn().mockImplementation(() => ({
action: mockAction
}))
));
I seem to have solved this by ensuring that the dependency to be mocked is a default export:
Provider.ts
:
export default class Provider {}
Consumer.ts
, Consumer.test.ts
:
import Provider from "../providers/provider";
I believe this is because jest.mock()
targets a module and provider
is a module with a class Provider
defined inside it. Without a default export in that module, the exact target for the mock is ambiguous. By making the class a default export, Jest knows to use it as the target for the mock.
Per the Jest documentation on Manual Mocks => Using With ES Module Imports, Jest sometimes requires to do work before the import. Under normal circumstances, it can "hoist" the mock before the import, even though in your code it appears after the import. However, with Typescript & ECMAScript support, it cannot do this. So for dynamic classes you would have to put your mock before the import.
But there are even more quirky things: if you are trying to mock a static method of your class, then that mock must be placed after the import.
So you end up with code like this:
... Dynamic class mock
... import statement for the dynamic class
... Mocks for static methods
Note that by import I also mean the import inside your consumer class .. i.e. your consumer class in your test file must be imported after the dynamic class mock but before any mocks for static methods.
You may be able to solve all this without resorting to placement by setting esModuleInterop Typescript piler option to true. However, this may have all sorts of side effects on code that is not patible and may be the more difficult route.