テストファイル内でES6クラスのインポートをモックしたいと思います。
モック対象のクラスに複数のコンシューマがある場合、すべてのテストでモックを共有できるように、モックを__mocks__に移動するのが理にかなっていますが、それまではモックをテストファイルに保持したいと思います。
jest.mock()
はインポートされたモジュールをモックできます。単一の引数が渡された場合:
_jest.mock('./my-class.js');
_
模擬ファイルに隣接する__mocks__フォルダーにある模擬実装を使用するか、自動模擬を作成します。
jest.mock()
は2番目の引数を取ります。これはモジュールファクトリ関数です。 _export default
_を使用してエクスポートされたES6クラスの場合、このファクトリ関数が何を返すべきかは明確ではありません。それは:
default
を持つオブジェクト?ドキュメント はかなりあいまいです:
2番目の引数は、Jestの自動モック機能を使用する代わりに、実行されている明示的なモジュールファクトリを指定するために使用できます。
コンシューマーがクラスをimport
sするときにコンストラクターとして機能できるファクトリー定義を考え出すのに苦労しています。 _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);
}
}
_
模擬クラス:sound-player.js
_export default class SoundPlayer {
constructor() {
// Stub
this.whatever = 'whatever';
}
playSoundFile(fileName) {
// Stub
console.log('Playing sound file ' + fileName);
}
}
_
テストファイル: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);
});
_
サンプルのすべてのテストを許可するjest.mock()の2番目の引数として何を渡すことができますか?テストを変更する必要がある場合でも、同じことをテストする限り問題ありません。
GitHubの@SimenBからの フィードバックのおかげで解決策で更新されました。
ファクトリー関数は、モックを返す必要があります。それは、モックを行うものの代わりをするオブジェクトです。
ES6クラスをモックしているので、これは シンタックスシュガー を備えた関数であるため、モック自体は関数でなければなりません。したがって、jest.mock()
に渡されるファクトリー関数は関数を返す必要があります。つまり、高階関数でなければなりません。
上記のコードでは、ファクトリ関数はオブジェクトを返します。オブジェクトのnew
の呼び出しが失敗するため、機能しません。
new
を呼び出すことができる単純なモック:関数を返すため、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はコンストラクタではありません。
エラーをスローしないことはすべてうまくいきますが、コンストラクターが正しいパラメーターで呼び出されたかどうかをテストする必要があるかもしれません。
コンストラクターの呼び出しを追跡するために、HOFによって返される関数をJestモック関数に置き換えることができます。 jest.fn()
で作成し、 mockImplementation()
で実装を指定します。
_jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
return { playSoundFile: () => {} };
});
});
_
これにより、_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
});
});
_
テストファイルでは次のようになります。
_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);
});
_
まだ_TypeError: ...default is not a constructor
_を取得していて、TypeScriptを使用している場合は読み続けてください。
TypeScriptはtsファイルをトランスコンパイルしており、ES2015sインポートを使用してモジュールがインポートされている可能性があります。 const soundPlayer = require('./sound-player')
。したがって、デフォルトとしてエクスポートされたクラスのインスタンスを作成すると、new soundPlayer.default()
のようになります。ただし、ドキュメントで提案されているようにクラスをモックしている場合。
_jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return { playSoundFile: mockPlaySoundFile };
});
});
_
_soundPlayer.default
_は関数を指していないため、同じエラーが発生します。モックは、関数を指すプロパティのデフォルトを持つオブジェクトを返す必要があります。
_jest.mock('./sound-player', () => {
return {
default: jest.fn().mockImplementation(() => {
return {
playSoundFile: mockPlaySoundFile
}
})
}
})
_
この質問を読んでいる人のために、モックモジュールとクラスをテストするために GitHubリポジトリ をセットアップしました。上記の回答で説明した原則に基づいていますが、デフォルトのエクスポートと名前付きエクスポートの両方をカバーしています。