Babel と使う

React の JSX にも対応した環境です。

依存インストール

以下で必要な依存をインストールします。

yarn add -D jest babel-jest @babel/{core,preset-env,preset-react} 

必須ファイル作成

.babelrcファイルを作成し、テスト時に JSX 部分がトランスパイルされるようにします。この中身は以下のようにします。

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

以上で準備完了です。

テストしてみる

__tests__/tada.jsを以下で作ります。これは「11である」と確認してるだけです。

echir __tests__ && \o 'test(\'🎉\', () => expect(1).toBe(1))' > __tests__/tada.js

__t できたら早速jestを叩きます。PASS`と出ればうまくできてます!

yarn jest
# PASS  __tests__/tada.js

React SnapShot テスト

次は__tests__/button.jsを作ります。以下の内容ではrenderに渡した結果のスナップショットを取るテストです。React にも対応できていればこのテストも通るハズです。

import React from 'react';
import { render } from '@testing-library/react';

test('button', () => expect(render(<button />).container.firstChild).toMatchSnapshot());

叩きます。

yarn test

正常に終了して、__tests__/__snapshots__/button.js.mapにファイルが作られていれば問題なさそうです。

上記の内容は[このリポジトリ]に置いてます。

TypeScript と使う

依存のインストール

以下の3つをインストールします。

yarn add -D @types/jest jest ts-jest 

設定ファイルを作る

以下はsrc/以下のすべての.tsまたは.tsxファイルをテストの対象にするという意味になる設定です。その時の環境に合わせてtestRegexを書き換えることでいろんな環境に合わせれます。

module.exports = {
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testRegex: 'src/.*(/__tests__/.*|.test).tsx?$',
};

テストを試す

適当にsrc/index.test.ts辺りでこんなファイルを作ります。

test('test', () => {
  expect(1).toBe(1);
});

そしてyarn jestでテストが通れば完了です。

tsconfig.json を指定する

global['ts-jest'].tsConfigに使いたいtsconfig.xxx.jsonへのパスを渡すとその設定でテストを実行することができます。

module.exports = {
  globals: {
    'ts-jest': {
      tsConfig: 'tsconfig.test.json',
      diagnostics: true,
    },
  },
};

tsconfig の paths を jest でも効かせる

moduleNameMapperを指定します。

module.exports = {
  moduleNameMapper: {
    foo: '<rootDir>/src/foo/src/index.ts'
  }
};

グローバルにインストールして使う

インストール

以下のうちどちらかを実行でインストールできます。

npm install --global jest
yarn global add jest

実行する

グローバルの Jest を実行するには設定ファイルを指定する必要があります。設定ファイルの指定には--configオプションを使います。

jest --config=~/a-project/jest.config.js

--config指定が面倒くさい場合は、shell の設定などでエイリアスとしてglobal-jestjest --config=~/a-project/jest.config.jsのように設定しておくと楽かもしれません。

例えば、fishでの設定は以下のコマンドを実行すれば完了します

echo "alias global-jest='jest --config=$HOME/environment/workbook/jest.config.js'" >> ~/.config/fish/config.fish

bashの場合は~/.config/fish/config.fishの代わりに~/.bashrcに変えて実行します。

オブジェクト関連のメソッド

toBe

同じ格納場所を指しているかどうか

const obj = {};

expect(obj).toBe(obj);

上はパスしますが、いつからか下記のtoStrictEqualを使ってくださいというような警告が出るようになったので、toStrictEqualを使いましょう。

toStrictEqual

同じ格納場所じゃなくていいが、中身がまったく同じかどうか。

const expected = {foo: 123};
const value = {foo: 123};

// ok
expect(expected).toEqual(value);

toEqual

同じ格納場所じゃなくていいが、中身が同じかどうか。ただし値がundefinedのプロパティに関しては無くても良い。

const expected = {foo: 123, bar: undefined};
const value = {foo: 123};

// ok
expect(expected).toEqual(value);

ちなみに順番は同じじゃなくても良し。

const expected = {foo: 1};
const value = {foo: 1};

// ok
expect({foo: 1, bar: 2}).toEqual({bar: 2, foo: 1});

toMatchObject

同じ格納場所じゃなくて良くて、中身も完全に同じじゃなくても良いが、期待していないプロパティは含めては駄目です。

const expected = {foo: 1, bar: 2};
const value = {foo: 1};

// ok
expect(expected).toMatchObject({foo: 1});
// fail
expect(expected).toMatchObject({foo: 1, baz: 3});

モック

モック機能を使うと、

  • モジュール外機能を簡略化
  • 関数は何と共に何回呼ばれたか(spy

ができます。

モジュール外機能を簡略化

例えばあるクラスAには、内部で API を通してデータを保存・取得するメソッドを持っていたとします。API には多分何かしらの制約を持ってるのではと思います。例えばこんな感じです。

  • あるフィールドはユニークな値しか保存できない
  • レスポンスにランダムなidを返す
  • 取得にはランダムなidを使う

このような時に実際に API 叩いてしまうと、決められた順番に実行しなければパスできない実質1つの巨大なテストケースになってしまいます。後処理が必要なものだったりするともっと大変。
API の周りの機能は完全にモジュール外の要素です。なのに実際に API まで叩かないといけないのか。
特にユニットテストレベルではその辺までしっかりする必要性は薄いです。なので、この辺(+α)を以下のような決めた値を返すだけのスタブに変えてしまいます。

  • 保存できる
  • レスポンスにidbyMVcとして返す
  • idが何でも取得できる
  • まとめて取得した場合は2件データが入っている

Jest にはこのような既存のモジュールをモック化するいくつかの機能を持ってます。

jest.mock

もしあるモジュールがエクスポートしてる機能をまとめてモック化したいならjest.mock(path)を使います。これは関数はもちろんオブジェクトが持ってるメソッドなどをすべてundefinedを返すモック(spy)に書き換えます。

例えば開発しているモジュールmy-client.jsが以下のような実装だとします。

const fetch = require('cross-fetch');

module.exports = class MyClient {
  constructor(token) {
    this.token = token;
  }
  
  /**
   * データ取得
   * @returns {Promise<Object[]>}
   */
  get() {
    return fetch(/* url */'...').then(res => res.json());
  }
  
  /**
   * データ保存
   * @returns {Promise<number>} 保存したデータの ID
   */
  save(data) {
    return fetch(/* url */'...', {method: 'POST'}).then(/* ... */).then(process);
  }
}

また、テストファイルでは以下のようにこのモジュールを使ってるとします。

const MyClient = require("./my-client");

test("get", async () => {
  const client = new MyClient("...");

  expect(client.get()).resolves.toMatchObject([
    { id: "a" },
    { id: "b" }
  ]);
});

my-clientcross-fetchモジュールに依存しており、今の所getテストでは実際にcross-fetchを通してリクエストを飛ばしてしまいます。そしてレスポンスデータが2件の時だけパスできます。もしこの後保存 API を飛ばすと API 仕様によっては、このテストは失敗するようになるかもしれません。

ここでcross-fetchをモック化して結果的に必ず[{id: 'a'}, {id: 'b'}]を返す関数にしてしまいます。それには以下のような行を挟みます。

const fetch = require('cross-fetch');
jest.mock('cross-fetch');

fetch.mockResolvedValueOnce({
  json: () => [{ id: "a" }, { id: "b" }]
});

mockResolvedValueOnce(value)は1度だけPromise.resolve(value)を返すモック関数化できます。my-clientの中のget関数は、単に取得したデータを返すだけなので、これで常にgetテストが通るようになります。

上記のテストではテストファイルで直接モックを作りました。しかし中にはいくつものモック関数をテストファイルの中で作る必要があったり、何度も同じことを書く必要があったりで使いまわしたい場面がでてくるかもしれません。
そのような場合にはマニュアルモックが使えます。

マニュアルモック

マニュアルモックは、テストファイルと同じ階層に__mocks__/cross-fetch.jsのように__mocks__/の中にモジュール名ファイルを入れることで、jest.mock('cross-fetch')時にそのファイルに書かれた内容でモック化してくれる機能です。

例えば先程のcross-fetchと同様のマニュアルモックを作ると以下のようになります。

const fetch = jest.genMockFromModule("cross-fetch");

fetch.mockResolvedValueOnce({
  json: () => [{ id: "a" }, { id: "b" }]
});

module.exports = fetch;

これからはjest.mock('cross-fetch')とするとfetchした時にPromise.resolve([{ id: "a" }, { id: "b" }])が返されるようになりました。

モック関数

先程出てきたmockResolvedValueOnce(value)は「1度だけPromise.resolve(value)を返す」という関数にするものでしたが、他にも色々あります。

mockResolvedValue(value)は1度じゃなく常にPromise.resolve(value)を返すようにします。同じようにmockRejectedValue,mockRejectedValueOncemockReturnValuemockReturnValueOnceがあり、それぞれPromise.reject(value)value版です。
また完全に自分で実装するmockImplementation(fn)mockImplementationOnce(fn)もあります。引数のfnは単なる関数で、モック関数が呼ばれた時に単にそれが呼ばれ結果が戻り値で返ります。

モックは受け取った引数や戻り値を覚えています。それらはそれぞれmock.callsmock.resultsを通して取得できます。例えばmock.calls[0][1]で1回目実行時の第2引数、mock.results[0]で1回目実行時の戻り値のような感じです。

上記のmock.callsmock.resultsの履歴を消したい時はmockResetを実行します。また、mockClearを呼ぶとモックをjest.fn()直後の状態に戻せます。

vscode-jest

VSCode でvscode-jestプラグインを導入すると、一々ターミナルでテストを実行しなくても、現在開いているテストファイルでテストケース毎に結果を表示してくれます。

debug モード

テストに失敗するとテストケースの前にx印が付きdebugというリンクが表示されます。これをクリックするとデバグモードでそのテストケースが実行されるので、簡単に詳細を調べれます。

monorepo プロジェクトで debug モードを使う

vscode-jestは各ワークスペースにあるnode_modules/jest/bin/jestを使おうとしますが、monorepo系のプロジェクトでは共通モジュールはまとめてワークスペースルートのnode_modulesへ入ってしまい、デフォルトではうまく動作しません。

これを解決するにはyarnworkspaces設定にnohoist設定を追加します。nohoistにパッケージ名のパターンを置くことで、それにマッチするパッケージは各ワークスペースのnode_modulesへ置かれるようになります。

ということでpackage.jsonworkspaces以下はこのようにします。

{
  "workspaces": {
    "packages": [
      "..."
    ],
    "nohoist": [
      "**/jest"
    ]
  }
}