Mark
Mark

Reputation: 12535

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:

Upvotes: 22

Views: 8913

Answers (3)

Christopher
Christopher

Reputation: 193

2024 Dec

I know there are a lot of really good answers on here. But I found this implementation to be the fastest to setup and easy to manipulate the outcome for multiple tests.

Sudo Type & Class

This would be the example of real classes, services, and types.

type User = { id: string; name: string }

interface Service {
    getUser(id: string): Promise<User | null>
}

class Class {
    constructor(private service: Service) {}
    async getUser(id: string): Promise<User | null> {
      return await this.service.getUser(id)
   }
}

Mock Service

  • Success
const mockService: Service = {
    getUser: jest.fn((id: string) => Promise.resolve({ id: id, name: "Jaz" })),
}

  • Fail
const mockService: Service = {
  getUser: jest.fn((id: string) => Promise.reject(new Error())),
}

Test

it("should return a user", async () => {
    const userPromise = new Class(mockService).getUser("123")
    expect(userPromise).resolves.toEqual({ id: "123", name: "Jaz" })
})

The goal of this would be to test the Class methods, not the Service layer. With this setup you could easily have many mock services that return anything you want.

Hopefully that is helpful to someone. :)

Upvotes: 0

Richard Matsen
Richard Matsen

Reputation: 23463

@anttix answer is best, but here is another angle that might be useful in other scenarios.

babel-plugin-rewire allows import Food from "./food"; to be over-ridden by the test.

First, yarn add babel-plugin-rewire

babel.config.js

const presets = [
  [
    "@babel/env",
    {
      targets: {
        node: 'current',
      },
    },
  ],
];

const plugins = [ 
  "babel-plugin-rewire"
];

module.exports = { presets, plugins };

meal_test.js

import getMeal from "../src/meal";
import Food from "../src/food";
import { __RewireAPI__ as RewireAPI } from "../src/meal";

describe("meal tests", () => {
  // beforeEach(() => {
  //   jest.resetModules();
  // });
  afterEach(() => {
    RewireAPI.__Rewire__('Food', Food)
  });

  it("should print dinner", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    const mockFood = {
      type: "breakfast",
      veg: "avocado",
      carbs: "toast"
    };
    RewireAPI.__Rewire__('Food', mockFood)

    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });

  it("should print dinner #2", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });
});

Upvotes: 0

anttix
anttix

Reputation: 7779

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.

Upvotes: 19

Related Questions