Jestテストの並行実行と逐次実行をちゃんと理解する


TL; DR (要約)

Jestの実行時オプション(CLIオプション)と、テストメソッド(it,test)の書き方で、並行処理と順番(逐次)処理をコントロールする方法について、以下の4パターンを図で説明します。

  1. 複数のテストファイルを並行処理し、ファイル内のテストは順番に実行する
  2. 複数のテストファイルを順番に処理し、ファイル内のテストも順番に実行する
  3. 複数のテストファイルを並行処理し、ファイル内のテストも並行実行する
  4. 複数のテストファイルを順番に処理し、ファイル内のテストは並行実行する

はじめに

Jestでは、パフォーマンスのために複数のテストファイルが並行に処理(テスト)されます。しかし、並行処理が思わぬエラーを生むこともあります。
たとえば、データベースへ接続するテストやpuppeteerを使ったE2Eテストなど副作用を生じさせるテストの場合、テストの書き方や実行タイミングによってはテストが失敗したり、想定しないテスト挙動の原因になります。
こういった実行タイミングによって成功したり失敗したりするエラーは原因の特定が難しいことが多いので、どこが並行に実行されているのかを知っておくのが良いでしょう。

また、後述しますがJestのデフォルトでは、テストファイルは並行処理されますがファイル内の各テストは順番に実行されるので、各テストを並行に実行する手法を知っておくとテスト実行時間を短縮できるかもしれません。

この記事では、Jestの実行時オプション(CLIオプション)と、テストメソッド(it,test)の書き方で、並行処理と順番(逐次)処理をコントロールする方法を紹介します。

なお、Jest自体の解説や、テストの実行順序に関連の深い[global]setup[Files[AfterEnv]], [global]teardown[Files[AfterEnv]], before[All|Each], after[All|Each]については説明しませんので、公式ドキュメント(Setup and Teardown)やその他の記事などで確認してください。ただ、参考までにこれらの実行タイミングだけは図中に記載しています。

1.複数のテストファイルを並行処理し、ファイル内のテストは順番に実行する

デフォルト状態でJestを実行すると、対象となる複数のテストファイルは並行に処理されます。このときの並行処理の上限数は、自動的に実行環境(マシン)のコア数が設定されます。
この上限数を変えるには--maxWorkers=:numberをCLIオプションに指定します。ただ、コア数以上の数字を指定してもパフォーマンス上の効果は薄く、通常は書き換えない方が良いとjest --helpには書かれています。

--maxWorkers, -w
Specifies the maximum number of workers the worker-pool will spawn for running tests. This defaults to the number of the cores available on your machine. (its usually best not to override this default)

また、ファイル内のテストはテストメソッドit, testを使用すると順番に実行されます。
describeでブロック化されている場合は、describeに潜っていく(深さ優先探索の)順序で実行されます。かんたんに言えば、テストファイルに書かれている順序です。

2.複数のテストファイルを順番に処理し、ファイル内のテストも順番に実行する

CLIオプションの--runInBandを付けてテストを実行すると、複数のテストファイルが1つずつ順番に処理されます。具体的にはテスト実行用ワーカーの子プロセスを生成せず、元のプロセス上で順次テストファイルを処理していきます。
これは前述の--maxWorkers=1を指定したことと同じで、はじめにで書いたようなテストコードのデバッグに役立つことはありますが、通常のテストとして利用するものではありません。

--maxWorkersと同様、jest --helpにも以下の記載があります。

--runInBand
Run all tests serially in the current process (rather than creating a worker pool of child processes that run tests). This is sometimes useful for debugging, but such use cases are pretty rare.

3.複数のテストファイルを並行処理し、ファイル内のテストも並行実行する

テストファイルの並行処理だけではなく、ファイル内のテストを並行に実行するには、テストメソッドを変更します。

// 順番に実行される
it('sequential it', () => {})
test('sequential test', () => {})

// 並行に実行される
it.concurrent('concurrently it', () => {})
test.concurrent('concurrently test', () => {})

CLIオプションで全体を制御するのではなく、一つ一つのテストについて並行実行するものは明示的に.concurrentを付けるかたちです。このメソッドは、jest-jasmine2で提供されているメソッドで並行実行可能な上限数の初期値は5です。

一つ一つメソッドで指定するのは、テストの実行時間によってはbeforeEachafterEachの実行タイミングがテスト毎に前後する可能性が高く、各テストを副作用のない互いに独立したテストとして実行する必要があるためです。たとえば、(あまり良い例ではありませんが)以下のようなテストコードは多くの場合失敗します。

let count = 0
beforeEach(() => { count++ }

it.concurrent('test1', (done) => {
  setTimeout(() => {
   expect(count).toBe(1) // fail: received count is maybe 2
   done()
  }, 300)
})

it.concurrent('test2', () => {
  expect(count).toBe(2) // pass
})

4.複数のテストファイルを順番に処理し、ファイル内のテストは並行実行する

最後は、テストファイルは順番に処理しつつ、そのファイル内の各テストは並行実行するパターンです。これは、上で説明したものの組み合わせですので図を見てもらえれば良いですね。

まとめとおわりに

以下の4パターンについて、テストの実行順をイメージ図で説明しました。実行方法をまとめると以下です。

  1. 複数のテストファイルを並行処理し、ファイル内のテストは順番に実行する
    • CLI:jest
    • テストメソッド:itまたはtest
  2. 複数のテストファイルを順番に処理し、ファイル内のテストも順番に実行する
    • CLI:jest --runInBand
    • テストメソッド:itまたはtest
  3. 複数のテストファイルを並行処理し、ファイル内のテストも並行実行する
    • CLI:jest
    • テストメソッド:it.concurrentまたはtest.concurrent
  4. 複数のテストファイルを順番に処理し、ファイル内のテストは並行実行する
    • CLI:jest --runInBand
    • テストメソッド:it.concurrentまたはtest.concurrent

今回説明した内容を理解しやすくするためにサンプルを用意しました。debugパッケージを使って、実行順のログを出力するものです。
https://github.com/noriaki/display-all-setup-teardown-lifecycle-of-jest

使い方は以下の通りです。質問や誤りなどあればお気軽にコメントください。
それでは、良いJestライフを!

# Setup
## リポジトリのクローン
 git clone https://github.com/noriaki/display-all-setup-teardown-lifecycle-of-jest.git

## クローンしたディレクトリに移動
 cd display-all-setup-teardown-lifecycle-of-jest

## パッケージインストール
 yarn install

# Usage
## 1.複数のテストファイルを並行処理し、ファイル内のテストは順番に実行する
 yarn test:sequential

## 2.複数のテストファイルを順番に処理し、ファイル内のテストも順番に実行する
 yarn test:sequential:run-in-band

## 3.複数のテストファイルを並行処理し、ファイル内のテストも並行実行する
 yarn test:concurrent

## 4.複数のテストファイルを順番に処理し、ファイル内のテストは並行実行する
 yarn test:concurrent:run-in-band