Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do we mock out dependencies with Jest per test?

Tags:

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:

  • thread the Food object around through getMeal + use dependency injection into formatMeal
    • (the whole point of this approach IRL is that we don't want to thread Food around the whole app)
  • manual mock + 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
    • Using 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.
like image 458
Mark Avatar asked Oct 31 '18 02:10

Mark


People also ask

How do you mock a dependent function in Jest?

Mock the module Instead of mocking every function, jest helps us mimic the entire module using jest. mock. Create mocks directory into the same path of the file to mock, export the functions, and create the module's name in our case weatherAPI. In our test, the to jest uses the mock module with jest.

How do you do mocking in Jest?

Mocking Modules​ export default Users; Now, in order to test this method without actually hitting the API (and thus creating slow and fragile tests), we can use the jest. mock(...) function to automatically mock the axios module.

How does Jest mock a module?

When a manual mock exists for a given module, Jest's module system will use that module when explicitly calling jest. mock('moduleName') . However, when automock is set to true , the manual mock implementation will be used instead of the automatically created mock, even if jest. mock('moduleName') is not called.

How do you create mock data in testing Jest?

In Jest, Node. js modules are automatically mocked in your tests when you place the mock files in a __mocks__ folder that's next to the node_modules folder. For example, if you a file called __mock__/fs. js , then every time the fs module is called in your test, Jest will automatically use the mocks.


1 Answers

Short answer

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({ ... });  ... 

Long answer

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.

Option 1a: Do not use ES6 modules from tests

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!");   }); }); 

Option 1b: Reload module on every invocation

One can also wrap the function under test.

Instead of

import getMeal from "../src/meal"; 

use

const getMeal = () => require("../src/meal").default(); 

Option 2: Register the mock and call through to real functions by 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.

like image 185
anttix Avatar answered Oct 21 '22 14:10

anttix