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
に保存されます。
クラスでアロー関数を使用する場合、それらはモックの一部には*なりません*。その理由は、アロー関数はオブジェクトのプロトタイプに存在せず、関数への参照を保持するプロパティに過ぎないためです。
クラスの実装を置き換える必要がない場合は、これが最も簡単な設定オプションです。例えば
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)
は、**モジュールファクトリ**引数を取ります。モジュールファクトリは、モックを返す関数です。
コンストラクタ関数をモックするには、モジュールファクトリはコンストラクタ関数を返す必要があります。言い換えれば、モジュールファクトリは関数を返す関数、つまり高階関数(HOF)である必要があります。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
jest.mock()
への呼び出しはファイルの先頭にホイストされるため、Jestはスコープ外の変数へのアクセスを防ぎます。デフォルトでは、最初に変数を定義してからファクトリで使用することはできません。Jestは、`mock`という単語で始まる変数に対してこのチェックを無効にします。ただし、それらが時間通りに初期化されることを保証するのは、依然としてあなたの責任です。一時的なデッドゾーンに注意してください。
たとえば、変数宣言で`mock`の代わりに`fake`を使用すると、次のコードはスコープ外のエラーをスローします。
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
変数宣言で`mock`を使用しているにもかかわらず、`mockSoundPlayer`はアロー関数でラップされていないため、ホイスト後に初期化される前にアクセスされるため、次のコードは`ReferenceError`をスローします。
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
mockImplementation()
またはmockImplementationOnce()
を使用してモックを置き換える
既存のモックでmockImplementation()
を呼び出すことにより、単一のテストまたはすべてのテストに対して実装を変更するために、上記のすべてのモックを置き換えることができます。
jest.mockへの呼び出しはコードの先頭にホイストされます。ファクトリパラメータを使用する代わりに、既存のモックでmockImplementation()
(またはmockImplementationOnce()
)を呼び出すことにより、後でモックを指定できます(例:beforeAll()
内)。これにより、必要に応じてテスト間でモックを変更することもできます。
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
詳細:モックコンストラクタ関数を理解する
jest.fn().mockImplementation()
を使用してコンストラクタ関数のモックを構築すると、モックが実際よりも複雑に見えます。このセクションでは、モックの仕組みを説明するために、独自のモックを作成する方法を示します。
別のES6クラスである手動モック
__mocks__
フォルダに、モックされたクラスと同じファイル名を使用してES6クラスを定義すると、それがモックとして機能します。このクラスは、実際のクラスの代わりに使用されます。これにより、クラスのテスト実装を挿入できますが、呼び出しをスパイする方法は提供されません。
人為的な例では、モックは次のようになります
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
モジュールファクトリパラメータを使用したモック
jest.mock(path, moduleFactory)
に渡されるモジュールファクトリ関数は、関数を返すHOFにすることができます*。これにより、モックで`new`を呼び出すことができます。繰り返しますが、これにより、テスト用に異なる動作を挿入できますが、呼び出しをスパイする方法は提供されません。
*モジュールファクトリ関数は関数を返す必要があります
コンストラクタ関数をモックするには、モジュールファクトリはコンストラクタ関数を返す必要があります。言い換えれば、モジュールファクトリは関数を返す関数、つまり高階関数(HOF)である必要があります。
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
アロー関数で`new`を呼び出すことはJavaScriptでは許可されていないため、モックはアロー関数にすることはできません。そのため、これは機能しません
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
コードがES5にトランスパイルされない限り(例:@babel/preset-env
による)、これは**_TypeError:_soundPlayer2.defaultはコンストラクタではありません**をスローします。(ES5にはアロー関数もクラスもないため、どちらもプレーン関数にトランスパイルされます。)
クラスの特定のメソッドをモックする
クラスSoundPlayer
内のメソッドplaySoundFile
をモックまたはスパイしたいとしましょう。簡単な例
// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"
it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});
静的メソッド、ゲッターメソッド、セッターメソッド
クラスSoundPlayer
にゲッターメソッドfoo
と静的メソッドbrand
があるとします
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}
簡単にモック/スパイできます。次に例を示します
// your jest test file below
import SoundPlayer from './sound-player';
const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');
const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');
it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();
expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});
使用状況の追跡(モックのスパイ)
テスト実装を挿入すると役立ちますが、クラスコンストラクタとメソッドが正しいパラメータで呼び出されているかどうかをテストすることもできます。
コンストラクタのスパイ
コンストラクタへの呼び出しを追跡するには、HOFから返された関数をJestモック関数に置き換えます。jest.fn()
で作成し、mockImplementation()
で実装を指定します。
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
これにより、SoundPlayer.mock.calls
を使用して、モックされたクラスの使用状況を検査できます。 expect(SoundPlayer).toHaveBeenCalled();
またはほぼ同等の:expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);
デフォルト以外のクラスエクスポートのモック
クラスがモジュールからのデフォルトのエクスポート*ではない*場合、クラスのエクスポート名と同じキーを持つオブジェクトを返す必要があります。
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
クラスのメソッドのスパイ
モックされたクラスは、テスト中に呼び出されるメンバー関数(この例では`playSoundFile`)を提供する必要があります。そうでない場合、存在しない関数を呼び出したというエラーが発生します。ただし、これらのメソッドへの呼び出しをスパイして、予期されたパラメータで呼び出されたことを確認することもできます。
テスト中にモックコンストラクタ関数が呼び出されるたびに、新しいオブジェクトが作成されます。 これらのすべてのオブジェクトのメソッド呼び出しをスパイするには、`playSoundFile`に別のモック関数を設定し、その同じモック関数への参照をテストファイルに保存して、テスト中に使用できるようにします。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
// Now we can track calls to playSoundFile
});
});
これに対応する手動モックは次のようになります。
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
使い方はモジュールファクトリ関数と似ていますが、jest.mock()
から2番目の引数を省略できる点と、モックされたメソッドはテストファイル内で定義されなくなるため、テストファイルにインポートする必要がある点が異なります。この際には、元のモジュールパスを使用し、__mocks__
を含めないでください。
テスト間のクリーンアップ
モックコンストラクタ関数とそのメソッドの呼び出し記録をクリアするには、beforeEach()
関数内で mockClear()
を呼び出します。
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
完全な例
jest.mock
にモジュールファクトリパラメータを使用する完全なテストファイルの例を以下に示します。
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
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.mock.calls[0][0]).toBe(coolSoundFileName);
});