Ainul Ξ Creative Dev

Ainul Ξ Creative Dev

Product-focused software engineer.
twitter
tg_channel

インポートを使った冗談: 安全か、それとも後悔か?

だから、私は自分の作業スペースでテストを確認していて、頭をかかえるような「require」パターンに出くわしました。

function getMocksForTest(): typeof import('./module-to-test') {
  // 必要なjestモックがまだ適用されていないため、モジュールのトップレベルでこれを単純にインポートすることはできません。
  return require('./module-to-test');
}

it('簡単なテストに合格する', () => {
  const { functionToTest } = getMocksForTest();
  expect(functionToTest()).toEqual('foo');
});

Jest を使用しているときに require 文に切り替える必要があったことはありませんので、これを見て驚きました。通常はモジュールを直接インポートするだけです。

import { functionToTest } from './module-to-test';

it('簡単なテストに合格する', () => {
  expect(functionToTest()).toEqual('foo');
});

インポート文を使用する方がずっと簡潔です。では、なぜ異なるパターンが使用されたのでしょうか?最初に参加したときはそのままにしておきましたが、今ではこのパターンがどこから生じたのか、いつ必要で、いつ削除できるのかを理解するためにいくつかの調査とソフトウェア考古学を行いました。

このパターンは jest.mock を使用する場合にのみ役立ちます#

私を困惑させた最初のことは、getMocksForTestという名前です。モックは関与していないので、その名前は適切ではないように思えました。しかし、後でより複雑なセットアップのテストファイルを見つけました。

function getMocksForTest() {
  const mockCreatePage = jest.fn();

  jest.mock('../pages', () => ({
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  }));

  // 必要なjestモックがまだ適用されていないため、モジュールのトップレベルでこれを単純にインポートすることはできません。
  const { functionToTest } =
    require('./module-to-test') as typeof import('./module-to-test');

  return {
    functionToTest,
    mockCreatePage,
  };
}

it('複雑なテストに合格する', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

ここでは、jest.mock()を使用して、ファイル../pages.tsのモジュールをモックしています。テストしているモジュール./module-to-test.ts../pages.tsに依存し、それをインポートしています。

jest.mock()を実行する前にテストするモジュールをインポートしてしまった場合、モックは適切に設定されません。その代わりに、../pages.tsの元のバージョンがインポートされてしまいます。

import { functionToTest } from './module-to-test';

function getMocksForTest() {
  const mockCreatePage = jest.fn();

  // 遅すぎる!この時点で`../pages`はすでにインポートされています。
  jest.mock('../pages', () => ({
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  }));

  return {
    mockCreatePage,
  };
}

実際にモックを作成する関数に出会った今、その名前はより意味を持ちます。後に、開発者たちはテストをコピー&ペーストし、必要ないものを削除するだけでした。これが、モックを一切行わない「get mocks」という名前の関数が生まれた理由です。

しかし、テストファイルがjest.mock()を一度も使用しない場合、このパターンは完全に不要です。クリーンアップして、モジュールを直接インポートすることができます。

jest.mock () は常にインポート文の後に実行されるのではないですか?#

このパターンが必要なのは、jest.mock()がモジュールがインポートされた後に実行されるときだとわかりました。しかし、多くのテストファイルはjest.mock()を使用しており、この require パターンが使われているのを見たことがありません。では、jest.mock()は通常どのように機能するのでしょうか?

実際、Jest はテストファイルを実行する前に特別な変換を行います。TypeScript を JavaScript に変換するだけでなく、jest.mock() 呼び出しをファイルの先頭に移動します、インポート文の前に。これは、デフォルトで有効になっているbabel-jestプラグインを使用して行われます。

その結果、次のようなテストファイルは:

// example.test.ts
import { functionToTest } from './module-to-test';

jest.mock('../pages', () => {
  const mockCreatePage: jest.MockedFn<() => string> = jest.fn();
  return {
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  };
});

it('複雑なテストに合格する', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

次のような Node.js 互換のファイルに変換されます:

// compliled.test.js
// ファイルの先頭に移動
jest.mock('../pages', () => {
  // TypeScriptの型が削除される
  const mockCreatePage = jest.fn();
  return {
    ...jest.requireActual('../pages'),
    createPage: mockCreatePage,
  };
});

// インポート文がCommonJSに変換される
const { functionToTest } = require('./module-to-test');

it('複雑なテストに合格する', () => {
  const { functionToTest, mockCreatePage } = getMocksForTest();
  mockCreatePage.mockReturnValue('dependency');
  expect(functionToTest()).toEqual('foo+dependency');
});

jest.mock は常にホイストされますか?#

いいえ!モジュールモックがホイストされるためには特定の条件が必要です。ここで、Jest の babel プラグインのソースコードを調べて見つけたチェックがあります。

  • jest.mock()の最初の引数はリテラル値である必要があります。
  • 2 番目の引数が提供されている場合、それはインライン関数である必要があります。
  • その関数は、関数の外で定義された変数を参照してはいけません。
  • jest.mock()はトップレベルで呼び出されなければならず、別の関数の内部では呼び出せません。
    • そうでない場合、呼び出された関数の先頭に移動されます。

基本的に、jest.mock()コードをファイルの先頭に単にカット&ペーストできない場合、自動的に移動されません。以前に定義された他の変数を参照すると、関数がホイストされなくなります。

// これらのいずれもホイストされません
const moduleName = './module';
jest.mock(moduleName);

function factory() {
  return jest.fn();
}
jest.mock('./module', factory);

jest.mock('./module', () => {
  // スコープの外で定義された変数
  return jest.fn().mockReturnValue(moduleName);
});

モック関数の外で定義された変数を参照する必要がある場合は、それらを別々にインポートするか、jest.doMockを使用してホイストを明示的に回避できます。

import { createPage } from '../pages';

jest.mock('../pages', () => ({
  createPage: jest.fn(),
}));

const mockCreatePage = createPage as jest.Mocked<typeof createPage>;

すべてのモジュールモックがホイストできる限り、インポート文を使用してテストするコードをインポートすることは安全です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。