Javascript jest.mock():如何使用工厂参数模拟 ES6 类默认导入

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/47402005/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 03:49:51  来源:igfitidea点击:

jest.mock(): How to mock ES6 class default import using factory parameter

javascriptecmascript-6jestjs

提问by stone

Mocking ES6 class imports

模拟 ES6 类导入

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

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

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.

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

Jest.mock()

Jest.mock()

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

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

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.

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

The module factory parameter

模块出厂参数

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

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

  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 defaultthat 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?
  1. 另一个返回模仿类实例的对象的函数?
  2. 模仿类实例的对象?
  3. 具有属性的对象default是返回模拟类实例的对象的函数?
  4. 一个函数返回一个高阶函数,它本身返回 1、2 或 3?

The docsare 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:

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

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).

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

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 defaultproperty (or not).

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

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

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

Class being tested: sound-player-consumer.js

正在测试的类: 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

被嘲笑的类: 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

测试文件: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.

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

回答by stone

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

感谢@SimenB 在 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.

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

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

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

Simple mock you can call newon:

您可以调用的简单模拟new

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

这是一个简单的版本,因为它返回一个函数,将允许调用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:

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

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.

这将抛出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 模拟函数替换 HOF 返回的函数。我们用 来创建它jest.fn(),然后用 来指定它的实现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.

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

Spying on methods of our class

监视我们班级的方法

Our mocked class will need to provide any member functions (playSoundFilein 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.

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

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

因为在我们的测试期间将创建一个新的模拟对象,对我们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
  });
});

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

回答by Santiago Ordonez

If you are still getting TypeError: ...default is not a constructorand are using TypeScript keep reading.

如果您仍在TypeError: ...default is not a constructor使用 TypeScript,请继续阅读。

TypeScript is transpiling your ts file and your module is likely being imported using ES2015s import. const soundPlayer = require('./sound-player'). Therefore creating an instance of the class that was exported as a default will look like this: new soundPlayer.default(). However if you are mocking the class as suggested by the documentation.

TypeScript 正在转换您的 ts 文件,并且您的模块很可能是使用 ES2015s import 导入的。 const soundPlayer = require('./sound-player'). 因此创建导出为默认看起来像这样的类的实例: new soundPlayer.default()。但是,如果您按照文档的建议模拟该课程。

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

You will get the same error because soundPlayer.defaultdoes not point to a function. Your mock has to return an object which has a property default that points to a function.

你会得到同样的错误,因为soundPlayer.default它没有指向一个函数。您的模拟必须返回一个对象,该对象具有指向函数的属性默认值。

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

For named imports, like import { OAuth2 } from './oauth', replace defaultwith imported module name, OAuth2in this example:

对于命名导入,如import { OAuth2 } from './oauth',替换default为导入的模块名称,OAuth2在本例中:

jest.mock('./oauth', () => {
    return {
        OAuth2: ... // mock here
    }
})

回答by nidkil

For anyone reading this question, I have setup a GitHub repositoryto test mocking modules and classes. It is based on the principles described in the answer above, but it covers both default and named exports.

对于阅读此问题的任何人,我已经设置了一个GitHub 存储库来测试模拟模块和类。它基于上述答案中描述的原则,但它涵盖了默认导出和命名导出。