我们如何用Jest _per test_模拟出依赖关系?

时间:2018-10-31 02:04:40

标签: javascript jestjs

(以下是完整的最小复制:https://github.com/magicmark/jest_question

给出以下应用程序:

  

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;
}

我进行了以下测试:

  

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

如何正确模拟Food 每次测试?换句话说,我只想将模拟应用于"should print breakfast (mocked)"测试用例。

我也不想改变应用程序的源代码(尽管也许 Food 是一个返回对象的函数是可以接受的-仍然不能使它正常工作。 )

我已经尝试过的事情:

  • 通过Food绕过getMeal对象,并使用依赖性注入formatMeal
    • (此方法IRL的全​​部要点是我们不想在整个应用程序中使用Food线程)
  • 手动模拟+ jest.mock()-答案可能在某处,但是由于导入时间怪异,很难在此处控制值并在每次测试中将其重置
    • 在顶部使用jest.mock()会覆盖每个测试用例,而我不知道如何在每次测试中更改或重置Food的值。

谢谢!

2 个答案:

答案 0 :(得分:8)

简短答案

设置模拟后, 后,使用require在每个测试功能中获取一个新模块。

it("should print breakfast (mocked)", () => {
    jest.doMock(...);
    const getMeal = require("../src/meal").default;

    ...
});

Food转换为函数并将对jest.mock的调用放入模块范围。

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

jest.mock("../src/food");
food.mockReturnValue({ ... });

...

长答案

Jest manual中有一个片段,内容为:

  

注意:为了正确模拟,Jest需要jest.mock('moduleName')与require / import语句处于同一范围。

same manual还指出:

  

如果您正在使用ES模块导入,则通常倾向于将import语句放在测试文件的顶部。但是通常您需要指示Jest在模块使用模拟之前使用它。因此,Jest会自动将jest.mock调用提升到模块的顶部(在导入之前)。

在执行任何测试功能之前,在模块范围内解析

ES6导入。因此,要应用模拟,需要在测试函数外部和导入任何模块之前声明它们。 Jest的Babel插件会将jest.mock语句“提升”到文件的开头,以便在导入之前执行它们。 请注意,jest.doMockdeliberately not hoisted

可以通过窥视Jest的缓存目录(运行jest --showConfig来了解位置)来研究生成的代码。

示例中的food模块很难模拟,因为它是对象文字而不是函数。最简单的方法是在每次需要更改值时强制重新加载模块。

选项1a:请勿使用测试中的ES6模块

ES6导入语句必须在模块范围内,但是“好旧的” require没有这种限制,可以从测试方法的范围中调用。

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

选项1b:每次调用都重新加载模块

也可以包装被测函数。

代替

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

使用

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

选项2:默认注册模拟并调用实函数

如果food模块公开了一个函数而不是一个文字,则可以对其进行模拟。模拟实例是可变的,可以在测试之间进行更改。

  

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!");
  });
});
当然,还有其他选择,例如将测试分为两个模块,其中一个文件设置模拟,另一个文件使用真实模块或返回可变对象代替food的默认导出。模块,以便可以通过每次测试对其进行修改,然后在beforeEach中手动重置。

答案 1 :(得分:0)

@anttix答案是最好的,但这是另一个角度,在其他情况下可能会有用。

babel-plugin-rewire允许import Food from "./food";被测试覆盖。

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