Jest ES6 类模拟

2021-09-18 20:26 更新

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

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

ES6 类示例

我们将使用一个播放声音文件的类的人为示例,​SoundPlayer​,以及使用该类的使用者类​SoundPlayerConsumer​。我们将​SoundPlayer​在我们的测试中模拟​SoundPlayerConsumer​.

  1. // sound-player.js
  2. export default class SoundPlayer {
  3. constructor() {
  4. this.foo = 'bar';
  5. }
  6. playSoundFile(fileName) {
  7. console.log('Playing sound file ' + fileName);
  8. }
  9. }
  1. // sound-player-consumer.js
  2. import SoundPlayer from './sound-player';
  3. export default class SoundPlayerConsumer {
  4. constructor() {
  5. this.soundPlayer = new SoundPlayer();
  6. }
  7. playSomethingCool() {
  8. const coolSoundFileName = 'song.mp3';
  9. this.soundPlayer.playSoundFile(coolSoundFileName);
  10. }
  11. }

创建 ES6 类模拟的 4 种方法

自动模拟

调用​jest.mock('./sound-player')​返回一个有用的“自动模拟”,你可以使用它来监视对类构造函数及其所有方法的调用。它取代了ES6类与模拟构造,并将其所有方法始终返回未定义的模拟函数。方法调用保存在​theAutomaticMock.mock.instances[index].methodName.mock.calls​.

请注意,如果你在类中使用箭头函数,它们将不会成为模拟的一部分。原因是箭头函数不存在于对象的原型中,它们只是持有对函数的引用的属性。

如果不需要替换类的实现,这是最容易设置的选项。例如:

  1. import SoundPlayer from './sound-player';
  2. import SoundPlayerConsumer from './sound-player-consumer';
  3. jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
  4. beforeEach(() => {
  5. // Clear all instances and calls to constructor and all methods:
  6. SoundPlayer.mockClear();
  7. });
  8. it('We can check if the consumer called the class constructor', () => {
  9. const soundPlayerConsumer = new SoundPlayerConsumer();
  10. expect(SoundPlayer).toHaveBeenCalledTimes(1);
  11. });
  12. it('We can check if the consumer called a method on the class instance', () => {
  13. // Show that mockClear() is working:
  14. expect(SoundPlayer).not.toHaveBeenCalled();
  15. const soundPlayerConsumer = new SoundPlayerConsumer();
  16. // Constructor should have been called again:
  17. expect(SoundPlayer).toHaveBeenCalledTimes(1);
  18. const coolSoundFileName = 'song.mp3';
  19. soundPlayerConsumer.playSomethingCool();
  20. // mock.instances is available with automatic mocks:
  21. const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
  22. const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
  23. expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  24. // Equivalent to above check:
  25. expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  26. expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
  27. });

手动模拟

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

  1. // __mocks__/sound-player.js
  2. // Import this named export into your test file:
  3. export const mockPlaySoundFile = jest.fn();
  4. const mock = jest.fn().mockImplementation(() => {
  5. return {playSoundFile: mockPlaySoundFile};
  6. });
  7. export default mock;

导入所有实例共享的模拟和模拟方法:

  1. // sound-player-consumer.test.js
  2. import SoundPlayer, {mockPlaySoundFile} from './sound-player';
  3. import SoundPlayerConsumer from './sound-player-consumer';
  4. jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
  5. beforeEach(() => {
  6. // Clear all instances and calls to constructor and all methods:
  7. SoundPlayer.mockClear();
  8. mockPlaySoundFile.mockClear();
  9. });
  10. it('We can check if the consumer called the class constructor', () => {
  11. const soundPlayerConsumer = new SoundPlayerConsumer();
  12. expect(SoundPlayer).toHaveBeenCalledTimes(1);
  13. });
  14. it('We can check if the consumer called a method on the class instance', () => {
  15. const soundPlayerConsumer = new SoundPlayerConsumer();
  16. const coolSoundFileName = 'song.mp3';
  17. soundPlayerConsumer.playSomethingCool();
  18. expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  19. });

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

jest.mock(path, moduleFactory)​接受一个模块工厂参数。模块工厂是一个返回模拟的函数。

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

  1. import SoundPlayer from './sound-player';
  2. const mockPlaySoundFile = jest.fn();
  3. jest.mock('./sound-player', () => {
  4. return jest.fn().mockImplementation(() => {
  5. return {playSoundFile: mockPlaySoundFile};
  6. });
  7. });

factory 参数的一个限制是,因为调用​jest.mock()​被提升到文件的顶部,所以不可能先定义一个变量然后在工厂中使用它。以单词“mock”开头的变量是一个例外。由您来保证它们会按时初始化!例如,由于在变量声明中使用了 'fake' 而不是 'mock',以下代码将抛出一个范围外错误:

  1. // Note: this will fail
  2. import SoundPlayer from './sound-player';
  3. const fakePlaySoundFile = jest.fn();
  4. jest.mock('./sound-player', () => {
  5. return jest.fn().mockImplementation(() => {
  6. return {playSoundFile: fakePlaySoundFile};
  7. });
  8. });

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

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

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

  1. import SoundPlayer from './sound-player';
  2. import SoundPlayerConsumer from './sound-player-consumer';
  3. jest.mock('./sound-player');
  4. describe('When SoundPlayer throws an error', () => {
  5. beforeAll(() => {
  6. SoundPlayer.mockImplementation(() => {
  7. return {
  8. playSoundFile: () => {
  9. throw new Error('Test error');
  10. },
  11. };
  12. });
  13. });
  14. it('Should throw an error when calling playSomethingCool', () => {
  15. const soundPlayerConsumer = new SoundPlayerConsumer();
  16. expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
  17. });
  18. });

深入:理解模拟构造函数

使用​jest.fn().mockImplementation()​构建构造函数​​模拟会使模拟看起来比实际更复杂。本节介绍了如何创建自己的模拟,来说明模拟的工作原理。

另一个 ES6 类的手动模拟

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

对于人为的示例,模拟可能如下所示:

  1. // __mocks__/sound-player.js
  2. export default class SoundPlayer {
  3. constructor() {
  4. console.log('Mock SoundPlayer: constructor was called');
  5. }
  6. playSoundFile() {
  7. console.log('Mock SoundPlayer: playSoundFile was called');
  8. }
  9. }

使用模块工厂参数模拟

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

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

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

  1. jest.mock('./sound-player', () => {
  2. return function () {
  3. return {playSoundFile: () => {}};
  4. };
  5. });

注意:箭头函数不起作用

请注意,模拟不能是箭头函数,因为newJavaScript 中不允许调用箭头函数。所以这行不通:

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

这将抛出​TypeError: _soundPlayer2.default is not a constructor​,除非代码被转换为 ES5,例如通过​@babel/preset-env​. (ES5 没有箭头函数和类,所以两者都将被转换为普通函数。)

跟踪使用情况(监视模拟)

注入测试实现很有帮助,但您可能还想测试是否使用正确的参数调用了类构造函数和方法。

监视构造函数

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

  1. import SoundPlayer from './sound-player';
  2. jest.mock('./sound-player', () => {
  3. // Works and lets you check for constructor calls:
  4. return jest.fn().mockImplementation(() => {
  5. return {playSoundFile: () => {}};
  6. });
  7. });

这将让我们检查模拟类的使用情况,使用​SoundPlayer.mock.calls​:​expect(SoundPlayer).toHaveBeenCalled();​或接近等效的:​expect(SoundPlayer.mock.calls.length).toEqual(1);

模拟非默认类导出

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

  1. import {SoundPlayer} from './sound-player';
  2. jest.mock('./sound-player', () => {
  3. // Works and lets you check for constructor calls:
  4. return {
  5. SoundPlayer: jest.fn().mockImplementation(() => {
  6. return {playSoundFile: () => {}};
  7. }),
  8. };
  9. });

监视我们类的方法

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

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

  1. import SoundPlayer from './sound-player';
  2. const mockPlaySoundFile = jest.fn();
  3. jest.mock('./sound-player', () => {
  4. return jest.fn().mockImplementation(() => {
  5. return {playSoundFile: mockPlaySoundFile};
  6. // Now we can track calls to playSoundFile
  7. });
  8. });

与此等效的手动模拟将是:

  1. // __mocks__/sound-player.js
  2. // Import this named export into your test file
  3. export const mockPlaySoundFile = jest.fn();
  4. const mock = jest.fn().mockImplementation(() => {
  5. return {playSoundFile: mockPlaySoundFile};
  6. });
  7. export default mock;

用法类似于模块工厂函数,不同之处在于您可以省略 from 的第二个参数​jest.mock()​,并且你必须将模拟方法导入到你的测试文件中,因为它不再在那里定义。为此使用原始模块路径;不包括​__mocks__​.

测试之间的清理

为了清除对模拟构造函数及其方法的调用记录,我们mockClear()在​beforeEach()​函数中调用:

  1. beforeEach(() => {
  2. SoundPlayer.mockClear();
  3. mockPlaySoundFile.mockClear();
  4. });

完整示例

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

  1. // sound-player-consumer.test.js
  2. import SoundPlayerConsumer from './sound-player-consumer';
  3. import SoundPlayer from './sound-player';
  4. const mockPlaySoundFile = jest.fn();
  5. jest.mock('./sound-player', () => {
  6. return jest.fn().mockImplementation(() => {
  7. return {playSoundFile: mockPlaySoundFile};
  8. });
  9. });
  10. beforeEach(() => {
  11. SoundPlayer.mockClear();
  12. mockPlaySoundFile.mockClear();
  13. });
  14. it('The consumer should be able to call new() on SoundPlayer', () => {
  15. const soundPlayerConsumer = new SoundPlayerConsumer();
  16. // Ensure constructor created the object:
  17. expect(soundPlayerConsumer).toBeTruthy();
  18. });
  19. it('We can check if the consumer called the class constructor', () => {
  20. const soundPlayerConsumer = new SoundPlayerConsumer();
  21. expect(SoundPlayer).toHaveBeenCalledTimes(1);
  22. });
  23. it('We can check if the consumer called a method on the class instance', () => {
  24. const soundPlayerConsumer = new SoundPlayerConsumer();
  25. const coolSoundFileName = 'song.mp3';
  26. soundPlayerConsumer.playSomethingCool();
  27. expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  28. });


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号