javascriptjestjsmocking

How do we mock out dependencies with Jest per test?


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:


Solution

  • 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.