【中文翻译】ES6 Class Mocks

原文地址

Jest 可以用来模拟导入到要测试的文件中的 ES6 类

ES6 类是带有一些语法糖的构造函数。因此,任何 ES6 类的 mock 必须是一个函数或一个实际的 ES6 类(同样是另一个函数)。你可以用 mock 函数来模拟它们。

An ES6 Class Example

我们将使用一个虚构的例子,一个播放声音文件的类,SoundPlayer,和一个使用该类的消费类,SoundPlaverConsumer。我们将在SoundPlayerConsumer的测试中模拟soundplver

// sound-player.js
export default class SoundPlayer {
  constructor() {
    this.foo = "bar";
  }

  playSoundFile(fileName) {
    console.log("Playing sound file " + fileName);
  }
}
// sound-player-consumer.js
import SoundPlayer from "./sound-player";

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer();
  }

  playSomethingCool() {
    const coolSoundFileName = "song.mp3";
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}

创建 ES6 类模拟的 4 种方法

Automatic mock 自动模拟

调用 jest.mock(../sound-player')将返回一个有用的“自动 mock”,您可以使用它来监视对类构造函数及其所有方法的调用。它用模拟构造函数替换 ES6 类,并用总是返回 undefined 的模拟函数替换 ES6 类的所有方法。方法调用保存在 automaticmock .mock 中。实例 [索引] . 方法名. 模拟. 呼叫。

请注意,如果在类中使用箭头函数,它们将不会是 mock 的一部分。这样做的原因是箭头函数并不存在于对象的原型中,它们仅仅是持有函数引用的属性。

如果你不需要替换类的实现,这是最简单的设置选项,例如:

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player");
// SoundPlayer 现在是一个模拟构造函数

beforeEach(() => {
  // 清除所有实例和调用构造函数和所有方法:
  SoundPlayer.mockClear();
});

it("我们可以检查使用者是否调用了类构造函数", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("我们可以检查使用者是否在类实例上调用了方法", () => {
  // 显示mockclear()正在工作:
  expect(SoundPlayer).not.toHaveBeenCalled();

  const soundPlayerConsumer = new SoundPlayerConsumer();
  // 构造函数应该被再次调用:
  expect(SoundPlayer).toHaveBeenCalledTimes(1);

  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();

  // 模拟。实例可用的自动模拟:
  const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
  const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  // 相当于上述检查:
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

Manual mock 人工模拟

通过将模拟实现保存在mocks文件夹中来创建手动模拟。这允许您指定实现,并且它可以跨测试文件使用。

// __mocks__/sound-player.js

// 将这个名为export的文件导入到测试文件中:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
  return { playSoundFile: mockPlaySoundFile };
});

export default mock;

Import the mock and the mock method shared by all instances:

// sound-player-consumer.test.js
import SoundPlayer, { mockPlaySoundFile } from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player");
// SoundPlayer现在是一个模拟构造函数

beforeEach(() => {
  // 清除所有实例和调用构造函数和所有方法:
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

it("我们可以检查使用者是否调用了类构造函数", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("我们可以检查使用者是否在类实例上调用了方法", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

使用模块工厂参数调用 jest.mock()

jest.mock(path, moduleFactory)接受一个模块工厂参数。模块工厂是一个返回模拟的函数。 为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是一个返回函数的函数——高阶函数(HOF)。

import SoundPlayer from "./sound-player";
const mockPlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

factory 参数的一个限制是,由于对 jest.mock()的调用被挂起到文件的顶部,所以不可能先定义一个变量,然后在工厂中使用它。以“mock”开头的变量会出现异常。这取决于你保证他们将被初始化的时间!例如,由于在变量声明中使用了'fake'而不是'mock',下面的语句将抛出一个超出作用域的错误:

// Note: this will fail
import SoundPlayer from "./sound-player";
const fakePlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: fakePlaySoundFile };
  });
});

使用mockImplementation()mockImplementationOnce()替换模拟

您可以通过在现有模拟上调用 mockImplementation()来替换上述所有模拟,以更改单个测试或所有测试的实现。

调用 jest.mock 被吊到代码的顶部。您可以稍后指定一个 mock,例如在 beforeAll()中,通过在现有的 mock 上调用 mockImplementation()(或 mockImplementationOnce())而不是使用工厂参数。如果需要,这还允许您在测试之间更改模拟:

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";

jest.mock("./sound-player");

describe("When SoundPlayer throws an error", () => {
  beforeAll(() => {
    SoundPlayer.mockImplementation(() => {
      return {
        playSoundFile: () => {
          throw new Error("Test error");
        }
      };
    });
  });

  it("当调用playSomethingCool时应该抛出一个错误吗", () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
  });
});

深入:理解模拟构造函数

使用 jest.fn(). mockimplementation()构建构造函数模拟会使模拟看起来比实际更复杂。本节展示如何创建自己的模拟,以说明模拟是如何工作的。

手动模拟是另一个 ES6 类

如果你定义一个 ES6 类使用与mock文件夹中的 mock 类相同的文件名,它将作为 mock。这个类将被用来代替真正的类。这允许您为类注入一个测试实现,但不提供监视调用的方法。

对于这个人为的示例,mock 可能是这样的:

// __mocks__/sound-player.js
export default class SoundPlayer {
  constructor() {
    console.log("Mock SoundPlayer: constructor was called");
  }

  playSoundFile() {
    console.log("Mock SoundPlayer: playSoundFile was called");
  }
}

使用模块工厂参数进行模拟

传递给 jest 的模块工厂函数。mock(path, moduleFactory)可以是一个返回函数*的 HOF。这将允许在 mock 上调用 new。同样,这允许您为测试注入不同的行为,但不提供监视调用的方法。

模块工厂函数必须返回一个函数

为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是一个返回函数的函数——高阶函数(HOF)。

jest.mock("./sound-player", () => {
  return function () {
    return { playSoundFile: () => {} };
  };
});

注意:箭头函数不起作用

请注意,mock 不能是箭头函数,因为在 JavaScript 中不允许对箭头函数调用 new。所以这行不通:

jest.mock("./sound-player", () => {
  return () => {
    // Does not work; arrow functions can't be called with new
    return { playSoundFile: () => {} };
  };
});

这将抛出 TypeError: _soundPlayer2.default 不是构造函数,除非代码被转译到 ES5,例如@babel/preset-env。(ES5 没有箭头函数和类,所以它们都将被转换为普通函数。)

跟踪使用情况(监视 mock)

注入测试实现是有帮助的,但您可能还需要测试类构造函数和方法是否使用正确的参数调用。 监视建造者#

为了跟踪对构造函数的调用,将 HOF 返回的函数替换为 Jest 模拟函数。用 jest.fn()创建它,然后用 mockImplementation()指定它的实现。

import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
  // Works and lets you check for constructor calls:
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: () => {}};
  });
});
This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); or near-equivalent: expect(SoundPlayer.mock.calls.length).toEqual(1);

模拟非默认的类导出

如果该类不是模块的默认导出,则需要返回一个与类导出名称相同的键的对象。

import { SoundPlayer } from "./sound-player";
jest.mock("./sound-player", () => {
  //可以检查构造函数调用:
  return {
    SoundPlayer: jest.fn().mockImplementation(() => {
      return { playSoundFile: () => {} };
    })
  };
});

监视我们班的方法

mock 类将需要提供在测试期间调用的任何成员函数(示例中的 playSoundFile),否则我们将在调用不存在的函数时得到一个错误。但我们可能还想监视对这些方法的调用,以确保它们是用预期的参数调用的。

每次在测试期间调用模拟构造函数时,都会创建一个新对象。为了监视所有这些对象中的方法调用,我们用另一个模拟函数填充 playSoundFile,并在测试文件中存储对同一模拟函数的引用,以便在测试期间可用。

import SoundPlayer from "./sound-player";
const mockPlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
    // Now we can track calls to playSoundFile
  });
});

对应的手动模拟是:

// __mocks__/sound-player.js

// 将这个命名的导出导入到测试文件中
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
  return { playSoundFile: mockPlaySoundFile };
});

export default mock;

用法类似于模块工厂函数,除了可以省略 jest.mock()的第二个参数,而且必须将 mock 方法导入到测试文件中,因为那里不再定义它了。为此使用原始模块路径;不包括mocks

清理测试之间的内容

要清除对模拟构造函数及其方法的调用记录,可以在 beforeEach()函数中调用 mockClear():

beforeEach(() => {
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

完整的示例

下面是一个完整的测试文件,它使用模块工厂参数来 jest.mock:

// sound-player-consumer.test.js
import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";

const mockPlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

beforeEach(() => {
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

it("消费者应该能够在SoundPlayer上调用new()", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  // 确保构造器创建了对象:
  expect(soundPlayerConsumer).toBeTruthy();
});

it("我们可以检查使用者是否调用了类构造函数", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("我们可以检查使用者是否在类实例上调用了方法", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});