web-dev-qa-db-ja.com

jest.mock():factoryパラメーターを使用してES6クラスのデフォルトのインポートをモックする方法

ES6クラスのインポートのモック

テストファイル内でES6クラスのインポートをモックしたいと思います。

モック対象のクラスに複数のコンシューマがある場合、すべてのテストでモックを共有できるように、モックを__mocks__に移動するのが理にかなっていますが、それまではモックをテストファイルに保持したいと思います。

Jest.mock()

jest.mock()はインポートされたモジュールをモックできます。単一の引数が渡された場合:

_jest.mock('./my-class.js');
_

模擬ファイルに隣接する__mocks__フォルダーにある模擬実装を使用するか、自動模擬を作成します。

モジュールファクトリーパラメーター

jest.mock()2番目の引数を取ります。これはモジュールファクトリ関数です。 _export default_を使用してエクスポートされたES6クラスの場合、このファクトリ関数が何を返すべきかは明確ではありません。それは:

  1. クラスのインスタンスを模倣するオブジェクトを返す別の関数?
  2. クラスのインスタンスを模倣するオブジェクト?
  3. クラスのインスタンスを模倣するオブジェクトを返す関数であるプロパティdefaultを持つオブジェクト?
  4. それ自体が1、2、または3を返す高次関数を返す関数?

ドキュメント はかなりあいまいです:

2番目の引数は、Jestの自動モック機能を使用する代わりに、実行されている明示的なモジュールファクトリを指定するために使用できます。

コンシューマーがクラスをimportsするときにコンストラクターとして機能できるファクトリー定義を考え出すのに苦労しています。 _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番目の引数として何を渡すことができますか?テストを変更する必要がある場合でも、同じことをテストする限り問題ありません。

17
stone

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);
});
_
18
stone

まだ_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 
            }   
        })
    }
})
_
3

この質問を読んでいる人のために、モックモジュールとクラスをテストするために GitHubリポジトリ をセットアップしました。上記の回答で説明した原則に基づいていますが、デフォルトのエクスポートと名前付きエクスポートの両方をカバーしています。

1
nidkil