简体   繁体   English

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

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

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. jest.mock()可以模拟导入的模块。 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.它使用在与模拟文件相邻的 __mocks__ 文件夹中找到的模拟实现,或者创建一个自动模拟。

The module factory parameter模块出厂参数

jest.mock() takes a second argument which is a module factory function. jest.mock()接受第二个参数,它是一个模块工厂函数。 For ES6 classes exported using export default , it's not clear what this factory function should return.对于使用export default导出的 ES6 类,不清楚这个工厂函数应该返回什么。 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?具有属性default值的对象是返回模拟类实例的对象的函数?
  4. A function that returns a higher-order function that itself returns 1, 2 or 3?一个函数返回一个高阶函数,它本身返回 1、2 或 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:第二个参数可用于指定正在运行的显式模块工厂,而不是使用 Jest 的自动模拟功能:

I'm struggling to come up with a factory definition that can function as a constructor when the consumer import s the class.我正在努力想出一个工厂定义,当消费者import类时,它可以充当构造函数。 I keep getting TypeError: _soundPlayer2.default is not a constructor (for example).我不断收到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 default property (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?我可以将什么作为第二个参数传递给 jest.mock() 以允许示例中的所有测试通过? 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.感谢@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.由于我们正在模拟一个 ES6 类,它是一个带有一些语法糖的函数,那么模拟本身必须是一个函数。 Therefore the factory function passed to jest.mock() must return a function;因此传递给jest.mock()的工厂函数必须返回一个函数; 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.由于在对象上调用new失败,它不起作用。

Simple mock you can call new on:您可以调用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;请注意,我们的模拟不能是箭头函数,因为我们不能在 Javascript 中对箭头函数调用 new; 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 .这将抛出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.为了跟踪对构造函数的调用,我们可以用 Jest 模拟函数替换 HOF 返回的函数。 We create it with jest.fn() , and then we specify its implementation with mockImplementation() .我们用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检查SoundPlayer.mock.calls类的使用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.我们的模拟类需要提供在我们的测试期间将被调用的任何成员函数(示例中的playSoundFile ),否则我们将在调用不存在的函数时出错。 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.因为在我们的测试期间将创建一个新的模拟对象, SoundPlayer.playSoundFile.calls不会帮助我们。 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.为了解决这个问题,我们用另一个模拟函数填充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);
});

If you are still getting TypeError: ...default is not a constructor and 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. TypeScript 正在转换您的 ts 文件,并且您的模块很可能是使用 ES2015s import 导入的。 const soundPlayer = require('./sound-player') . 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() .因此,创建作为默认导出的类的实例将如下所示: new soundPlayer.default() However if you are mocking the class as suggested by the documentation.但是,如果您按照文档的建议模拟该课程。

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

You will get the same error because soundPlayer.default does not point to a function.你会得到同样的错误,因为soundPlayer.default没有指向一个函数。 Your mock has to return an object which has a property default that points to a function.您的模拟必须返回一个对象,该对象具有指向函数的属性默认值。

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

For named imports, like import { OAuth2 } from './oauth' , replace default with imported module name, OAuth2 in this example:对于命名导入,例如import { OAuth2 } from './oauth' ,在本例中用导入的模块名称OAuth2替换default

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

For anyone reading this question, I have setup a GitHub repository to test mocking modules and classes.对于阅读此问题的任何人,我已经设置了一个GitHub 存储库来测试模拟模块和类。 It is based on the principles described in the answer above, but it covers both default and named exports.它基于上述答案中描述的原则,但它涵盖了默认导出和命名导出。

If you have defined a mocking class, you can use something like:如果你定义了一个模拟类,你可以使用类似的东西:

jest.mock("../RealClass", () => {
  const mockedModule = jest.requireActual(
    "../path-to-mocked-class/MockedRealClass"
  );
  return {
    ...mockedModule,
  };
});

The code will do something like replacing method and property definitions of the original RealClass, with the one of MockedRealClass.代码会做一些事情,比如用 MockedRealClass 之一替换原始 RealClass 的方法和属性定义。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM