メインコンテンツへスキップ
バージョン: 29.7

ES6クラスのモック

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

Jestを使用すると、テスト対象ファイルにインポートされるES6クラスをモック化できます。

ES6クラスは構文糖衣を伴ったコンストラクタ関数です。したがって、ES6クラスのモックは関数または実際のES6クラス(これも関数です)でなければなりません。モック関数を使用してモック化できます。

ES6クラスの例

ここでは、サウンドファイルを再生するSoundPlayerクラスと、それを使用するSoundPlayerConsumerクラスの例を用います。SoundPlayerConsumerのテストでSoundPlayerをモック化します。

sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}

playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
sound-player-consumer.js
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__フォルダにモック実装を保存して手動モックを作成します。これにより実装を指定でき、複数のテストファイルで使用できます。

__mocks__/sound-player.js
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});

export default mock;

モックと全インスタンスで共有されるモックメソッドをインポートします:

sound-player-consumer.test.js
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)モジュールファクトリ引数を受け取ります。モジュールファクトリはモックを返す関数です。

コンストラクタ関数をモックするには、モジュールファクトリがコンストラクタ関数を返す必要があります。つまり、モジュールファクトリは関数を返す関数(高階関数)でなければなりません。

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
注意

jest.mock()呼び出しはファイル先頭に巻き上げられるため、Jestはスコープ外変数へのアクセスを制限します。通常、変数を定義してからファクトリ内で使用することはできません。mockで始まる変数はこの制限が解除されますが、適切な初期化を保証するのは開発者の責任です。Temporal Dead Zoneに注意してください。

例えば次の例は、変数宣言で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クラスを定義すると、それがモックとして機能します。このクラスは実際のクラスの代わりに使用され、テスト実装を注入できますが、呼び出しを監視する機能は提供されません。

作為的な例では、モックは次のようになります:

__mocks__/sound-player.js
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 を呼び出せますが、こちらも呼び出しの監視機能は提供されません。

* モジュールファクトリ関数は関数を返さなければならない

コンストラクタ関数をモックするには、モジュールファクトリがコンストラクタ関数を返す必要があります。つまり、モジュールファクトリは関数を返す関数(高階関数)でなければなりません。

jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
メモ

モックをアロー関数にすることはできません。JavaScriptではアロー関数に対して new を呼び出すことが許可されていないためです。以下のコードは動作しません:

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

この場合、TypeError: _soundPlayer2.default is not a constructor がスローされます(@babel/preset-env などで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();
});

使用状況の追跡(モックのスパイ)

テスト実装の注入は有用ですが、クラスコンストラクタやメソッドが正しいパラメータで呼び出されているかテストしたい場合もあるでしょう。

コンストラクタのスパイ

コンストラクタの呼び出しを追跡するには、高階関数が返す関数を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
});
});

この手動モックの同等実装は次のようになります:

__mocks__/sound-player.js
// 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() の第二引数を省略できる点と、モックメソッドをテストファイルにインポートする必要がある点が異なります(モックは元の場所で定義されなくなったため)。この際は元のモジュールパスを使用し、__mocks__ を含めないでください。

テスト間のクリーンアップ

モックコンストラクター関数とそのメソッドへの呼び出し記録をクリアするには、beforeEach() 関数内で mockClear() を呼び出します:

beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

完全な例

以下は jest.mock のモジュールファクトリパラメーターを使用した完全なテストファイルです:

sound-player-consumer.test.js
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);
});