🎭

Playwrightの並行実行の設定を見てみよう

2024/03/29に公開

こんにちは!saimyonです👶

今回はPlaywrighの並行処理について書きます🎭

なんで?

Playwrightの強みの1つであるテストの並行実行ですが、深く考えずに使っていたらハマってしまいました…
そこから、公式ドキュメントを読んで実験をしてみて理解を深めたので、その結果を記事にしている次第です。

並行実行に関する設定

Playwrightでは、複数のテスト(テストファイル)を、複数のworkerによって並行実行することが可能です。
公式のParallelizeのページに書かれている並行実行に関する設定は主に2パターンです。

  1. configファイル内のfullyParallelで設定
  2. 各テストファイル内でtest.describe.configureを使って設定

それぞれについて確認していきます。

configファイル内のfullyParallelで設定

まずはconfigファイル内での設定方法です。

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // ~~~他の設定~~~
  fullyParallel: true,
  // ~~~他の設定~~~
});

上記のように、fullyParallelで設定します。
この設定によって、テストファイル内のテストが並行実行がされます。
インストール時点ではtrueになっています。

実際に動かして確認してみましょう!
テストファイル(parallel.spec.ts)を用意しました。

parallel.spec.ts
import { test } from "@playwright/test";
import * as path from "node:path";

test("test1", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
});
test("test2", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});
test("test3", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});

test.info().workerIndexでworkerのindexが取得できるので、どのテストをどのworkerが処理しているのかをログで表示します。
まずはfullyParallelfalseにして実行してみます。

result
Running 3 tests using 1 worker
parallel.spec.ts:4:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:9:5 › test2
parallel.spec.ts > test2 > worker index:  0  start
parallel.spec.ts > test2 > worker index:  0  finished
parallel.spec.ts:15:5 › test3
parallel.spec.ts > test3 > worker index:  0  start
parallel.spec.ts > test3 > worker index:  0  finished
  3 passed (6.8s)

indexが0のworker1台で、test1->test2->test3の順番で実行されてますね。

それではfullyParalleltrueにして実行してみます。

result
Running 3 tests using 3 workers
parallel.spec.ts:15:5 › test3
parallel.spec.ts > test3 > worker index:  2  start
parallel.spec.ts:9:5 › test2
parallel.spec.ts > test2 > worker index:  1  start
parallel.spec.ts:4:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts:15:5 › test3
parallel.spec.ts > test3 > worker index:  2  finished
parallel.spec.ts:9:5 › test2
parallel.spec.ts > test2 > worker index:  1  finished
parallel.spec.ts:4:5 › test1
parallel.spec.ts > test1 > worker index:  0  finished
  3 passed (2.7s)

index0~2の3台のworkerによって、並行実行されてる様子が見受けられます。
実行時間も短くなり、効率的にテストできててHappyですね🥰
結果を見る限り、実行優先度は記述順とかにはよらず、順不同…なんですかね?(実装がどうなっているかまでは追えてません…)

また、workerの台数も設定可能です。

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // ~~~他の設定~~~
  workers: process.env.CI ? 2 : undefined,
  // ~~~他の設定~~~
});

テスト実行時のオプションでも指定できます。

npx playwright test --workers 4

デフォルトは、論理CPUコア数の半分とのことです。

test.d.tsのコメントより
Defaults to half of the number of logical CPU cores.

各テストファイル内でtest.describe.configureを使って設定

続いてはテストファイル内での設定方法です。
テストファイル内ではtest.describe.configure()modeで設定します

example.spec.ts
import { test } from '@playwright/test';

test.describe.configure({ mode: 'parallel' });

// ~~~テスト実装~~~

modeは、parallel/serial/defaultの3パターンがあります。
それぞれ実際に動かして試してみます。

parallel

まずはparallelの場合です。こちらはその名の通り並行実行されます。
先ほどのテストファイルに追記して実行してみます。

parallel.spec.ts
import { test } from "@playwright/test";
import * as path from "node:path";

test.describe.configure({ mode: 'parallel' });

test("test1", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
});
test("test2", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});
test("test3", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});

実行!

result
Running 3 tests using 3 workers
parallel.spec.ts:17:5 › test3
parallel.spec.ts > test3 > worker index:  2  start
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  1  start
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  1  finished
parallel.spec.ts:17:5 › test3
parallel.spec.ts > test3 > worker index:  2  finished
  3 passed (3.0s)

並行実行されてますね!
fullyParallelfalseに設定されていますが、その設定はこちらで上書きされます。

default/serial

続いてはdefault/serialです。
いずれも直列実行されますが、defaultは途中でテストが落ちてしまっても後続のテストが実行されるのに対し、serialは途中でテストが落ちてしまうとテスト全体はそこで終了されます。
確認してみましょう。

まずはこれまでのテストファイル内でdefaultに設定して、実行してみます。

parallel.spec.ts
import { test } from "@playwright/test";
import * as path from "node:path";

test.describe.configure({ mode: 'default' });

test("test1", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
});
test("test2", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});
test("test3", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});

実行!

result
Running 3 tests using 1 worker
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  0  start
parallel.spec.ts > test2 > worker index:  0  finished
parallel.spec.ts:17:5 › test3
parallel.spec.ts > test3 > worker index:  0  start
parallel.spec.ts > test3 > worker index:  0  finished
  3 passed (6.6s)

fullyParallelfalseのときと同じく、直列実行されてますね。
それではtest2を失敗するようにして実行してみましょう。

parallel.spec.ts
import {expect, test} from "@playwright/test";
import * as path from "node:path";

test.describe.configure({ mode: 'default' });

test("test1", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
});
test("test2", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await expect(1).toBe(0);
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});
test("test3", async ({ page }) => {
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    await page.waitForTimeout(2000);
    console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')

});

実行!

result
Running 3 tests using 1 worker
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  0  start
  1) parallel.spec.ts:11:5 › test2 ─────────────────────────────────────────────────────────────────

    Error: expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: 1

      11 | test("test2", async ({ page }) => {
      12 |     console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    > 13 |     await expect(1).toBe(0);
         |                     ^
      14 |     await page.waitForTimeout(2000);
      15 |     console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
      16 |

        at /hogehoge/parallel.spec.ts:13:21

parallel.spec.ts:18:5 › test3
parallel.spec.ts > test3 > worker index:  1  start
parallel.spec.ts > test3 > worker index:  1  finished
  1 failed
    parallel.spec.ts:11:5 › test2 ──────────────────────────────────────────────────────────────────
  2 passed (5.1s)

test2は失敗しましたが、test3は実行されています。

serialに設定して同じテストを実行してきます。
コードは省略しますが、まずは最後まで通るテストを実行!

Running 3 tests using 1 worker
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  0  start
parallel.spec.ts > test2 > worker index:  0  finished
parallel.spec.ts:17:5 › test3
parallel.spec.ts > test3 > worker index:  0  start
parallel.spec.ts > test3 > worker index:  0  finished
  3 passed (6.9s)

その名の通り直列実行されています。
ここでtest2が失敗するようにすると…

Running 3 tests using 1 worker
parallel.spec.ts:6:5 › test1
parallel.spec.ts > test1 > worker index:  0  start
parallel.spec.ts > test1 > worker index:  0  finished
parallel.spec.ts:11:5 › test2
parallel.spec.ts > test2 > worker index:  0  start
  1) parallel.spec.ts:11:5 › test2 ─────────────────────────────────────────────────────────────────

    Error: expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: 1

      11 | test("test2", async ({ page }) => {
      12 |     console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' start')
    > 13 |     await expect(1).toBe(0);
         |                     ^
      14 |     await page.waitForTimeout(2000);
      15 |     console.log(path.basename(test.info().file),'>', test.info().title, '> worker index: ', test.info().workerIndex, ' finished')
      16 |

        at /hogehoge/parallel.spec.ts:13:21

  1 failed
    parallel.spec.ts:11:5 › test2 ──────────────────────────────────────────────────────────────────
  1 skipped
  1 passed (2.7s)

test3が実行されてません!
ドキュメントでもserialの使用は推奨されていないので、直列実行したい場合はdefaultを使うのがいいかもしれません。

何にハマったの?

冒頭でも触れた、ハマったポイントについてです。

当時、関連する複数のテスト同士をtest.describeを使って1つのグループにくくるようなテストを書きました。

example.spec.ts
test.describe("test group 1", () => {
    test("test1", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
    test("test2", async ({ page }) => {
      // test1で保存したアイテムを使ったテスト
    });
});
test.describe("test group 2", () => {
    test("test3", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
    test("test4", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
});

ひとまとまりになることでわかりやすくなりますよね。
test group 1において、test2ではtest1で保存したアイテムを使用するため、test1->test2の順番でテスト実行する必要があります。

そしてこのときの私は「Playwrightでは、テストは基本的には(デフォルトでは)上から直列で実行されていくんだろう」となぜか思いこんでおり、「これでテストは通るはず!」と実行したらtest group 1のテストが通らない場合がありました…
その原因がなかなか特定できず、そこで少しハマってしまいました。

そんな中Parallelizeのページに辿り着き、ここまでのような実験をして、

example.spec.ts
test.describe("test group 1", () => {
    test.describe.configure({ mode: 'default' });
    test("test1", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
    test("test2", async ({ page }) => {
      // test1で保存したアイテムを使ったテスト
    });
});
test.describe("test group 2", () => {
    test("test3", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
    test("test4", async ({ page }) => {
      // 何らかのアイテムを保存するテスト
    });
});

このようにtest group 1test.describe内でmodedefaultに設定するに至りました。
ここまででは紹介していませんでしたが、test.describe内でもmodeの設定はできます。
上記のように設定することで、test group 1test group 2は並行実行しつつ、test group 1内のテストは直列実行することができます。

これでテストが最後まで通るようになり、事なきを得ました。
まずはちゃんと公式ドキュメントを読みましょう。大事。(再認識)

まとめ

今回はPlaywrightの並行実行について、実験を通して確認してみました。
これで並行実行を使いこなして、効率的なE2Eテストの自動化が実現できますね!

それでは!よいE2E自動テストライフを!

ソーシャルデータバンク テックブログ

Discussion