ES6 クラスのモック
Jest は、テストしたいファイルにインポートした ES6 クラスをモックすることもできます。
ES6 クラスというのは、いくつかの糖衣構文を加えたコンストラクタ関数です。 したがって、ES6 クラスのモックは、何らかの関数であるか、もう一つの ES6 クラス (繰り返しますが、これは別の関数です) になります。 そのため、モック関数を使用することでモックを作成できます。
ES6 クラスの例
具体例として、音楽ファイルを再生するクラス SoundPlayer
とそのクラスを使用する消費者クラス SoundPlayerConsumer
について考えてみましょう。 SoundPlayerConsumer
のテスト内で、SoundPlayer
クラスをモックするには、次のようなコードを書きます。
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
ES6 クラスのモックを作る4つの方法
自動モック
jest.mock('./sound-player')
を呼ぶと、便利な "自動モック" を返してくれます。 これは、クラスのコンストラクタおよびすべてのメソッドの呼び出しをスパイするのに使用できます。 この関数は ES6 クラスをモックコンストラクタに置き換え、すべてのメソッドを、常に undefined
を返すモック関数に置き換えます。 メソッドの呼び出しは theAutomaticMock.mock.instances[index].methodName.mock.calls
に保存されます。
If you use arrow functions in your classes, they will not be part of the mock. なぜなら、アロー関数はオブジェクトのプロトタイプには現れず、単に関数への参照を保持しているプロパティに過ぎないためです。
クラスの実装を置き換える必要がない場合は、このメソッドを使用するのが最も簡単な選択肢です。 例:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
// Show that mockClear() is working:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Constructor should have been called again:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances is available with automatic mocks:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
マニュアルモック
マニュアルモックを作るには、モックの実装を __mocks__
ディレクトリに保存します。 これにより、特定の実装をテストファイル全体で使用することができるようになります。
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
すべてのインスタンスで共有される、モックとモックメソッドをインポートします。
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
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).toHaveBeenCalledWith(coolSoundFileName);
});
jest.mock()
をモジュールファクトリ引数で呼ぶ
jest.mock(path, moduleFactory)
はモジュールファクトリ引数を取ります。 モジュールファクトリとは、モックを返す関数のことです。
コンストラクタ関数をモックするために は、モジュールファクトリはコンストラクタ関数を返さなければなりません。 言い換えると、モジュールファクトリは関数を返す関数、つまり高階関数 (high-order function; HOF) でなければなりません。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
Since calls to jest.mock()
are hoisted to the top of the file, Jest prevents access to out-of-scope variables. By default, you cannot first define a variable and then use it in the factory. Jest will disable this check for variables that start with the word mock
. However, it is still up to you to guarantee that they will be initialized on time. Be aware of Temporal Dead Zone.
For example, the following will throw an out-of-scope error due to the use of fake
instead of mock
in the variable declaration.
// 注意: これは失敗します
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
The following will throw a ReferenceError
despite using mock
in the variable declaration, as the mockSoundPlayer
is not wrapped in an arrow function and thus accessed before initialization after hoisting.
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});