Here's the full minimal repro
Given the following app:
src/food.js
const Food = {
carbs: "rice",
veg: "green beans",
type: "dinner"
};
export default Food;
src/food.js
import Food from "./food";
function formatMeal() {
const { carbs, veg, type } = Food;
if (type === "dinner") {
return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`;
} else if (type === "breakfast") {
return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`;
} else {
return "No soup for you!";
}
}
export default function getMeal() {
const meal = formatMeal();
return meal;
}
I have the following test:
_tests_/meal_test.js
import getMeal from "../src/meal";
describe("meal tests", () => {
beforeEach(() => {
jest.resetModules();
});
it("should print dinner", () => {
expect(getMeal()).toBe(
"Good evening. Dinner is green beans and rice. Yum!"
);
});
it("should print breakfast (mocked)", () => {
jest.doMock("../src/food", () => ({
type: "breakfast",
veg: "avocado",
carbs: "toast"
}));
// prints out the newly mocked food!
console.log(require("../src/food"));
// ...but we didn't mock it in time, so this fails!
expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
});
});
How do I correctly mock out Food
per test? In other words, I only want to apply the mock for the "should print breakfast (mocked)"
test case.
I would also like to not change the application source code ideally (although maybe having Food
be a function that returns an object instead would be acceptable - still can't get that to work either)
Things I've tried already:
Food
object around through getMeal
+ use dependency injection into formatMeal
Food
around the whole app)jest.mock()
- it's possible the answer is here somewhere, but it's tough to control the value here and reset it per test due to import time weirdness
jest.mock()
at the top would override it for every test case, and I can't work out how to then change or reset the value of Food
per test.Use require
to grab a fresh module in every test function after setting up mocks.
it("should print breakfast (mocked)", () => {
jest.doMock(...);
const getMeal = require("../src/meal").default;
...
});
or
Turn Food
into a function and put a call to jest.mock
into module scope.
import getMeal from "../src/meal";
import food from "../src/food";
jest.mock("../src/food");
food.mockReturnValue({ ... });
...
There is a snippet in Jest manual that reads:
Note: In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement.
The same manual also states:
If you're using ES module imports then you'll normally be inclined to put your import statements at the top of the test file. But often you need to instruct Jest to use a mock before modules use it. For this reason, Jest will automatically hoist jest.mock calls to the top of the module (before any imports).
ES6 imports are resolved in the module scope before any of the test functions execute. Thus for mocks to be applied, they need to be declared outside of test functions and before any modules are imported. Jest's Babel plugin will "hoist" jest.mock
statements to the beginning of the file so they are executed before any imports take place. Note that jest.doMock
is deliberately not hoisted.
One can study the generated code by taking a peek into Jest's cache directory (run jest --showConfig
to learn the location).
The food
module in the example is difficult to mock because it is an object literal and not a function. The easiest way is to force a reload of the module every time the value needs to be changed.
ES6 import statements must be module scoped, however the "good old" require
has no such limitation and can be called from the scope of a test method.
describe("meal tests", () => {
beforeEach(() => {
jest.resetModules();
});
it("should print dinner", () => {
const getMeal = require("../src/meal").default;
expect(getMeal()).toBe(
"Good evening. Dinner is green beans and rice. Yum!"
);
});
it("should print breakfast (mocked)", () => {
jest.doMock("../src/food", () => ({
type: "breakfast",
veg: "avocado",
carbs: "toast"
}));
const getMeal = require("../src/meal").default;
// ...this works now
expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
});
});
One can also wrap the function under test.
Instead of
import getMeal from "../src/meal";
use
const getMeal = () => require("../src/meal").default();
If the food
module exposed a function and not a literal, it could be mocked. The mock instance is mutable and can be changed from test to test.
src/food.js
const Food = {
carbs: "rice",
veg: "green beans",
type: "dinner"
};
export default function() { return Food; }
src/meal.js
import getFood from "./food";
function formatMeal() {
const { carbs, veg, type } = getFood();
if (type === "dinner") {
return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`;
} else if (type === "breakfast") {
return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`;
} else {
return "No soup for you!";
}
}
export default function getMeal() {
const meal = formatMeal();
return meal;
}
__tests__/meal_test.js
import getMeal from "../src/meal";
import food from "../src/food";
jest.mock("../src/food");
const realFood = jest.requireActual("../src/food").default;
food.mockImplementation(realFood);
describe("meal tests", () => {
beforeEach(() => {
jest.resetModules();
});
it("should print dinner", () => {
expect(getMeal()).toBe(
"Good evening. Dinner is green beans and rice. Yum!"
);
});
it("should print breakfast (mocked)", () => {
food.mockReturnValueOnce({
type: "breakfast",
veg: "avocado",
carbs: "toast"
});
// ...this works now
expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
});
});
Of-course there are other options like splitting the test into two modules where one file sets up a mock and the other one uses a real module or returning a mutable object in place of a default export for the food
module so it can be modified by each test and then manually reset in beforeEach
.