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

sto*_*one 17 javascript ecmascript-6 jestjs

嘲弄ES6类进口

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

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

Jest.mock()

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

jest.mock('./my-class.js');
Run Code Online (Sandbox Code Playgroud)

它使用与模拟文件相邻的__mocks__文件夹中的模拟实现,或创建自动模拟.

模块工厂参数

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

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

文档很模糊:

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

我正在努力想出一个工厂定义,当消费者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);
  }
}
Run Code Online (Sandbox Code Playgroud)

被嘲笑的类: sound-player.js

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

  playSoundFile(fileName) {
    // Stub
    console.log('Playing sound file ' + fileName);
  }
}
Run Code Online (Sandbox Code Playgroud)

测试文件: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);
});
Run Code Online (Sandbox Code Playgroud)

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

San*_*nez 40

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

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

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

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

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

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

jest.mock('./oauth', () => {
    return {
        OAuth2: ... // mock here
    }
})
Run Code Online (Sandbox Code Playgroud)

  • 多谢!就我而言,我使用命名导入 `import { OAuth2 } from '...'`。我刚刚用“OAuth2:”替换了答案中的“default:”,它成功了! (3认同)

sto*_*one 20

感谢来自@SimenB在GitHub上的反馈,更新了解决方案.


工厂功能必须返回一个功能

工厂函数必须返回mock:取代它所嘲笑的对象.

因为我们正在嘲笑一个ES6类,这是一个具有一些语法糖的函数,所以模拟本身必须是一个函数.因此传递给的工厂函数jest.mock()必须返回一个函数; 换句话说,它必须是一个更高阶的函数.

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

你可以打电话给简单的模拟new:

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

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

注意:箭头功能不起作用

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

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

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

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

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

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

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

这将让我们检查我们的模拟类的使用情况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
  });
});
Run Code Online (Sandbox Code Playgroud)

完整的例子

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

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);
});
Run Code Online (Sandbox Code Playgroud)

  • 我仍然得到`TypeError:...默认不是构造函数`. (4认同)
  • 一些看起来微不足道的东西导致构造函数在模拟后缺乏定义 - ES6 类不是默认导出。在我将模拟类更新为默认导出然后分别更新导入之前,无法进行任何工作。事后模拟工作。 (2认同)