Jest + TypeScript でテストを書くことが増えてきたので、この辺でTipsを記載しておこうと思います。今後も何かあれば追記します。

インストール

こちら を参考にしました。

まず、必要なライブラリを追加します。

yarn init
yarn add jest @types/jest ts-jest --dev

jest.config.jsをプロジェクト直下に作成します。

module.exports = {
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

package.jsonにスクリプトを追加します。

  "scripts": {
    "test": "jest"
  },

あとは、以下で実行できます。

yarn test

フォルダ構成は以下のような感じです。

.
├── __tests__
├── dist
├── src
├── jest.config.js
├── package.json
├── tsconfig.json
└── yarn.lock

基本的な書き方

基本的な構成を紹介しておきます。自分は基本いつも以下のような感じで書いてます。
ポイントはソースコード中に記載しているので、そちらを参照ください。

  • src/jest/target.ts: テスト対象のコード

    import { thrice } from './other';
    
    /** 文字列を2回繰り返す */
    export const twice = (str: string): string => {
      return str + str;
    };
    
    /** 文字列を2回+3回で合計5回繰り返す */
    export const twiceAndThrice = (str: string): string => {
      return twice(str) + thrice(str);
    };
  • src/jest/other.ts: target.tsから呼び出される別のモジュール
    /** 文字列を3回繰り返す */
    export const thrice = (str: string): string => {
      return str + str + str;
    };
  • __tests__/jest/target.test.ts: テストコード

    import { twice, twiceAndThrice } from '../../src/jest/target';
    import { thrice } from '../../src/jest/other';
    
    // 対象のモジュールをモック
    jest.mock('../../src/jest/other');
    const mockThrice = thrice as jest.Mock; // typescriptの場合はキャストが必要
    
    /** 各テスト実行前に行われる共通処理 */
    beforeEach(() => {
      mockThrice.mockClear(); // モックをクリアする
    });
    
    /** describeでテストをグルーピングできる */
    describe('without mock', () => {
      test('twice', () => {
        const actual = twice('input'); // 実行
        expect(actual).toBe('inputinput'); // inputが2回繰り返された結果が返っている
      });
    });
    
    describe('with mock', () => {
      test('twiceAndThrice', () => {
        mockThrice.mockReturnValueOnce('mock'); // モック関数の戻り値を指定
        const actual = twiceAndThrice('input'); // 実行
        expect(actual).toBe('inputinputmock'); // 本来はinputが5回繰り返されるが、内部で呼び出しているthrice関数の戻り値部分がmockになっている
        expect(mockThrice.mock.calls.length).toBe(1); // mock関数の呼び出し回数チェック
        expect(mockThrice.mock.calls[0][0]).toBe('input'); // mock関数の1回目の呼び出しの第一引数チェック
      });
    });

少し補足すると、jest.mockは各テスト関数内ではなく、トップレベルで実行する必要があるようで、その点が結構不便です。トップレベルでモック化するので、モックは全てのテスト関数に影響を及ぼしますので、beforeEachなりでモックをクリアする必要があります。

Tips

モジュールの一部をモック

対象モジュールのある関数だけをモックし、それ以外の変数や関数は本物が動いて欲しい場合に、jest.mockを利用するとモジュール全体がモックされてしまい、都合が悪いです。
そのようなケースはjest.spyOnを使えば大丈夫です。

  • src/jest/target.ts: テスト対象のコード

    import { plusStatic } from './other';
    
    /** 文字列を2回繰り返し固定文字列を追加する */
    export const twicePlusStatic = (str: string): string => {
      return plusStatic(twice(str));
    };
  • src/jest/other.ts

    const staticStr = 'static';
    
    /** 文字列に固定文字列を追加して返す */
    export const plusStatic = (str: string): string => {
      return str + staticStr;
    };
  • __tests__/jest/target2.test.ts: テストコード

    import { twicePlusStatic } from '../../src/jest/target';
    import * as other from '../../src/jest/other'; // アスタリスクでモジュール全体に別名をつけておく
    
    test('real', () => {
      const actual = twicePlusStatic('input'); // 実行
      expect(actual).toBe('inputinputstatic'); // inputが2回+固定値が返っている
    });
    
    test('with spy', () => {
      const spyPlusStatic = jest.spyOn(other, 'plusStatic'); // 対象の関数をモック
      spyPlusStatic.mockReturnValueOnce('mock'); // モック関数の戻り値を指定
      const actual = twicePlusStatic('input'); // 実行
      expect(actual).toBe('mock'); // モック値が返っている
      expect(spyPlusStatic.mock.calls.length).toBe(1); // mock関数の呼び出し回数チェック
      expect(spyPlusStatic.mock.calls[0][0]).toBe('inputinput'); // mock関数の1回目の呼び出しの第一引数チェック
    });

少し補足すると、jest.spyOnの第一引数はオブジェクト、第二引数は関数名なので、import時に少し工夫して、モジュール全体をアスタリスク*でimportして別名をつけておき、その別名を第一引数に、実際にモックしたい関数名を第二引数に指定する必要があります。

また、モジュールではなく、クラスの一部の関数だけをモックしたい場合は、クラスをimportしておき、第一引数にクラスを、第二引数にモックしたい関数名を指定する形になり、こちらはシンプルです。

jest.spyOnの戻り値はSpyInstanceですがこれはMockInstanceを継承しているため、mock.callsmock.clearなど、jest.mockした時と同じように使うことができます。

また、jest.spyOnでモックした場合、関数スコープのようなので、jest.mockよりも正直使い勝手が良い気はしてます。

クラス内の(staticな)関数をモック

クラス内の関数をモックするのはjest.spyOnで普通にできます。
static関数でも問題ないです。

  • src/jest/target.ts: テスト対象のコード

    import OtherClass from "./otherClass";
    
    /** 文字列を2回繰り返し固定文字列を追加する(クラスバージョン) */
    export const twicePlusStaticClass = (str: string): string => {
      return OtherClass.plusStatic(twice(str));
    };
  • src/jest/otherClass.ts: target.tsから呼び出すクラス
    export default class OtherClass {
      static staticStr = 'staticClass';
      static plusStatic = (str: string): string => {
        return str + this.staticStr;
      }
    }
  • __tests__/jest/target3.test.ts: テストコード

    import { twicePlusStaticClass } from '../../src/jest/target';
    import OtherClass from '../../src/jest/otherClass'; // astariskでモジュール全体に別名をつけておく
    
    test('real', () => {
      const actual = twicePlusStaticClass('input');
      expect(actual).toBe('inputinputstaticClass'); // inputが2回+固定値が返っている
    });
    
    test('with spy', () => {
      const spyPlusStatic = jest.spyOn(OtherClass, 'plusStatic');
      spyPlusStatic.mockReturnValueOnce('mock');
      const actual = twicePlusStaticClass('input');
      expect(actual).toBe('mock'); // モック値が返っている
      expect(spyPlusStatic.mock.calls.length).toBe(1); // mock関数の呼び出し回数チェック
      expect(spyPlusStatic.mock.calls[0][0]).toBe('inputinput'); // mock関数の1回目の呼び出しの第一引数チェック
    });

上の例は、jest.spyOnの例ですが、jest.mockでももちろんモックできます。

import { twicePlusStaticClass } from '../../src/jest/target';
import OtherClass from '../../src/jest/otherClass';

// 対象のモジュールをモックする。関数の中ではなくトップレベルに記載必要
jest.mock('../../src/jest/otherClass');
const mockPlusStatic = OtherClass.plusStatic as jest.Mock; // typescriptの場合はキャストが必要

beforeEach(() => {
  mockPlusStatic.mockClear(); // モックをクリアする
});

test('with mock', () => {
  mockPlusStatic.mockReturnValueOnce('mock');
  const actual = twicePlusStaticClass('input');
  expect(actual).toBe('mock'); // モック値が返っている
  expect(mockPlusStatic.mock.calls.length).toBe(1); // mock関数の呼び出し回数チェック
  expect(mockPlusStatic.mock.calls[0][0]).toBe('inputinput'); // mock関数の1回目の呼び出しの第一引数チェック
});

async 関数のモックの戻り値指定

すぐ忘れるので、一応メモ。

jest.spyOn(other, 'plusStatic').mockReturnValue(Promise.resolve('hoge'));