jest.mock():如何使用factory参数模拟ES6类的默认导入 [英] jest.mock(): How to mock ES6 class default import using factory parameter

查看:76
本文介绍了jest.mock():如何使用factory参数模拟ES6类的默认导入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

模拟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?

文档非常模糊:


第二个参数可用于指定正在运行的显式模块工厂,而不是使用Jest的自动锁定功能:


我很难想出工厂定义当消费者 import 这个类时,at可以作为构造函数。我一直在 TypeError:_soundPlayer2.default不是构造函数(例如)。



我试过避免使用箭头功能(因为它们不能用 new 调用)并且工厂返回一个具有默认属性(或不具有)的对象。



这是一个例子。这不起作用;所有测试抛出 TypeError:_soundPlayer2.default不是构造函数



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

 从'./sound-player'导入SoundPlayer; //默认导入

导出默认类SoundPlayerConsumer {
constructor(){
this.soundPlayer = new SoundPlayer(); // TypeError:_soundPlayer2.default不是构造函数
}

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

被嘲笑的类:
sound-player.js

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

playSoundFile(fileName){
// Stub
console.log('播放声音文件'+ fileName);
}
}

测试文件:声音播放器 - consumer.test.js

 从'./sound-player-consumer'导入SoundPlayerConsumer; 
从'./sound-player'导入SoundPlayer;

//我可以传递什么作为第二个arg,
//允许下面的所有测试通过?
jest.mock('./ sound-player',function(){
return {
default:function(){
return {
playSoundFile:jest。 fn()
};
}
};
});

it('消费者应该能够在SoundPlayer上调用new()',()=> {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(soundPlayerConsumer ).toBeTruthy(); //构造函数运行时没有错误
});

it('我们可以检查消费者是否称为模拟类构造函数',()=> {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer) .toHaveBeenCalled();
});

it('我们可以检查消费者是否在类实例上调用了一个方法',()=> {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName ='song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(SoundPlayer.playSoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

我可以将第二个arg传递给jest.mock(),它将允许所有测试在示例中传递?如果测试需要修改就好 - 只要他们仍然测试相同的东西。

解决方案

更新感谢来自@SimenB在GitHub上的反馈。






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



工厂函数必须返回mock:取代它正在嘲笑的对象。



因为我们正在模拟一个ES6类,它是一个带有一些语法糖的函数,然后模拟必须是一个函数。因此传递给 jest.mock()的工厂函数必须返回一个函数;换句话说,它必须是一个更高阶的函数。



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



简单模拟你可以调用 new on:



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

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

注意:箭头功能不起作用



请注意,我们的模拟不能是箭头函数,因为我们无法在Javascript中使用箭头函数调用new;这是语言中固有的。所以这不起作用:

  jest.mock('./ sound-player',()=> {
return()=> {//不起作用;无法使用新的
返回调用箭头函数{playSoundFile :()=> {}};
};
});

这将抛出 TypeError:_soundPlayer2.default不是构造函数



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



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



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

  jest.mock('./ sound-player',()=> {
return jest.fn()。mockImplementation(()=> {//工作并允许您检查构造函数调用
return {playSoundFile :()=> {}};
});
});

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



监视我们班级的方法



我们的模拟课程将会需要提供将在我们的测试期间调用的任何成员函数(在示例中为 playSoundFile ),否则我们将因调用不存在的函数而出错。但是我们可能也希望监视对这些方法的调用,以确保使用预期的参数调用它们。



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

  let mockPlaySoundFile = jest.fn(); 
jest.mock('./ sound-player',()=> {
return jest.fn()。mockImplementation(()=> {//工作并让你检查构造函数调用
return {playSoundFile:mockPlaySoundFile}; //现在我们可以跟踪对playSoundFile的调用
});
});



完整示例



以下是它的外观在测试文件中:

 从'./sound-player-consumer'导入SoundPlayerConsumer; 
从'./sound-player'导入SoundPlayer;

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

it('消费者应该能够在SoundPlayer上调用new()',()=> {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(soundPlayerConsumer ).toBeTruthy(); //构造函数运行时没有错误
});

it('我们可以检查消费者是否称为类构造函数',()=> {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer)。 toHaveBeenCalled();
});

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


Mocking ES6 class imports

I'd like to mock my ES6 class imports within my test files.

If the class being mocked has multiple consumers, it may make sense to move the mock into __mocks__, so that all the tests can share the mock, but until then I'd like to keep the mock in the test file.

Jest.mock()

jest.mock() can mock imported modules. When passed a single argument:

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

it uses the mock implementation found in the __mocks__ folder adjacent to the mocked file, or creates an automatic mock.

The module factory parameter

jest.mock() takes a second argument which is a module factory function. For ES6 classes exported using export default, it's not clear what this factory function should return. Is it:

  1. Another function that returns an object that mimics an instance of the class?
  2. An object that mimics an instance of the class?
  3. An object with a property default that is a function that returns an object that mimics an instance of the class?
  4. A function that returns a higher-order function that itself returns 1, 2 or 3?

The docs are quite vague:

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

I'm struggling to come up with a factory definition that can function as a constructor when the consumer imports the class. I keep getting TypeError: _soundPlayer2.default is not a constructor (for example).

I've tried avoiding use of arrow functions (since they can't be called with new) and having the factory return an object that has a default property (or not).

Here's an example. This is not working; all of the tests throw TypeError: _soundPlayer2.default is not a constructor.

Class being tested: 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);
  }
}

Class being mocked: sound-player.js

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

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

The test file: 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);
});

What can I pass as the second arg to jest.mock() that will allow all of the tests in the example pass? If the tests need to be modified that's okay - as long as they still test for the same things.

解决方案

Updated with a solution thanks to feedback from @SimenB on GitHub.


Factory function must return a function

The factory function must return the mock: the object that takes the place of whatever it's mocking.

Since we're mocking an ES6 class, which is a function with some syntactic sugar, then the mock must itself be a function. Therefore the factory function passed to jest.mock() must return a function; in other words, it must be a higher-order function.

In the code above, the factory function returns an object. Since calling new on the object fails, it doesn't work.

Simple mock you can call new on:

Here's a simple version that, because it returns a function, will allow calling new:

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

Note: Arrow functions won't work

Note that our mock can't be an arrow function because we can't call new on an arrow function in Javascript; that's inherent in the language. So this won't work:

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

This will throw TypeError: _soundPlayer2.default is not a constructor.

Keeping track of usage (spying on the mock)

Not throwing errors is all well and good, but we may need to test whether our constructor was called with the correct parameters.

In order to track calls to the constructor, we can replace the function returned by the HOF with a Jest mock function. We create it with jest.fn(), and then we specify its implementation with mockImplementation().

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

This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls.

Spying on methods of our class

Our mocked class will need to provide any member functions (playSoundFile in the example) that will be called during our tests, or else we'll get an error for calling a function that doesn't exist. But we'll probably want to also spy on calls to those methods, to ensure that they were called with the expected parameters.

Because a new mock object will be created during our tests, SoundPlayer.playSoundFile.calls won't help us. To work around this, we populate playSoundFile with another mock function, and store a reference to that same mock function in our test file, so we can access it during tests.

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

Complete example

Here's how it looks in the test file:

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

这篇关于jest.mock():如何使用factory参数模拟ES6类的默认导入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆