jjzjj

javascript - 开 Jest (): How to mock ES6 class default import using factory parameter

coder 2024-05-05 原文

模拟 ES6 类导入

我想在我的测试文件中模拟我的 ES6 类导入。

如果被模拟的类有多个消费者,将模拟移动到 __mocks__ 中可能是有意义的,这样所有的测试都可以共享模拟,但在那之前我想将模拟保留在测试文件中。

Jest.mock()

jest.mock() 可以模拟导入的模块。当传递单个参数时:

jest.mock('./my-class.js');

它使用在与模拟文件相邻的 __mocks__ 文件夹中找到的模拟实现,或创建一个自动模拟。

模块出厂参数

jest.mock() 采用第二个参数,这是一个模块工厂 函数。 对于使用export default导出的ES6类,不清楚这个工厂函数应该返回什么。是不是:

  1. 返回模拟类实例的对象的另一个函数?
  2. 模拟类实例的对象?
  3. 具有属性 default 的对象是一个返回模拟类实例的对象的函数?
  4. 返回高阶函数的函数,而高阶函数本身返回 1、2 或 3?

The docs非常模糊:

The second argument can be used to specify an explicit module factory that is being run instead of using Jest's automocking feature:

当消费者import类时,我正在努力想出一个可以充当构造函数的工厂定义。我不断收到 TypeError: _soundPlayer2.default is not a constructor(例如)。

我已经尝试避免使用箭头函数(因为它们不能用 new 调用)并让工厂返回一个具有 default 属性的对象(或不)。

这是一个例子。这是行不通的;所有测试都抛出 TypeError: _soundPlayer2.default is not a constructor

正在测试的类: sound-player-consumer.js

import SoundPlayer from './sound-player'; // Default import

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer(); //TypeError: _soundPlayer2.default is not a constructor
  }

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

被 mock 的类: sound-player.js

export default class SoundPlayer {
  constructor() {
    // Stub
    this.whatever = 'whatever';
  }

  playSoundFile(fileName) {
    // Stub
    console.log('Playing sound file ' + fileName);
  }
}

测试文件:sound-player-consumer.test.js

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

// What can I pass as the second arg here that will 
// allow all of the tests below to pass?
jest.mock('./sound-player', function() { 
  return {
    default: function() {
      return {
        playSoundFile: jest.fn()
      };
    }
  };
});

it('The consumer should be able to call new() on SoundPlayer', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
});

it('We can check if the consumer called the mocked class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
});

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(SoundPlayer.playSoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

我可以将什么作为第二个参数传递给 jest.mock() 以允许示例中的所有测试通过?如果测试需要修改也没关系 - 只要它们仍然测试相同的东西。

最佳答案

感谢 feedback from @SimenB on GitHub. 更新了解决方案


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

工厂函数必须返回模拟对象:代替模拟对象的对象。

因为我们正在模拟一个 ES6 类,它是 a function with some syntactic sugar ,那么模拟本身必须是一个函数。因此传递给 jest.mock() 的工厂函数必须返回一个函数;换句话说,它必须是高阶函数。

在上面的代码中,工厂函数返回一个对象。由于在对象上调用 new 失败,因此它不起作用。

您可以调用 new 的简单模拟:

这是一个简单的版本,因为它返回一个函数,所以将允许调用 new:

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 is not a constructor

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

不抛出错误当然很好,但我们可能需要测试我们的构造函数是否使用正确的参数调用。

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

jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
    return { playSoundFile: () => {} };
  });
});

这将让我们使用 SoundPlayer.mock.calls 检查模拟类的使用情况。

监视我们类的方法

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

因为在我们的测试期间将创建一个新的模拟对象,SoundPlayer.playSoundFile.calls 不会帮助我们。为了解决这个问题,我们用另一个模拟函数填充 playSoundFile,并在我们的测试文件中存储对同一个模拟函数的引用,这样我们就可以在测试期间访问它。

let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
    return { playSoundFile: mockPlaySoundFile }; // Now we can track calls to playSoundFile
  });
});

完整示例

这是它在测试文件中的样子:

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

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

it('The consumer should be able to call new() on SoundPlayer', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
});

it('We can check if the consumer called the class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
});

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});

关于javascript - 开 Jest (): How to mock ES6 class default import using factory parameter,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47402005/

有关javascript - 开 Jest (): How to mock ES6 class default import using factory parameter的更多相关文章

随机推荐