Playwrightの並行実行の設定を見てみよう
こんにちは!saimyonです👶
今回はPlaywrighの並行処理について書きます🎭
なんで?
Playwrightの強みの1つであるテストの並行実行ですが、深く考えずに使っていたらハマってしまいました…
そこから、公式ドキュメントを読んで実験をしてみて理解を深めたので、その結果を記事にしている次第です。
並行実行に関する設定
Playwrightでは、複数のテスト(テストファイル)を、複数のworkerによって並行実行することが可能です。
公式のParallelizeのページに書かれている並行実行に関する設定は主に2パターンです。
- configファイル内の
fullyParallel
で設定 - 各テストファイル内で
test.describe.configure
を使って設定
それぞれについて確認していきます。
configファイル内のfullyParallelで設定
まずはconfigファイル内での設定方法です。
import { defineConfig } from '@playwright/test';
export default defineConfig({
// ~~~他の設定~~~
fullyParallel: true,
// ~~~他の設定~~~
});
上記のように、fullyParallel
で設定します。
この設定によって、テストファイル内のテストが並行実行がされます。
インストール時点ではtrue
になっています。
実際に動かして確認してみましょう!
テストファイル(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が処理しているのかをログで表示します。
まずはfullyParallel
をfalse
にして実行してみます。
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
の順番で実行されてますね。
それではfullyParallel
をtrue
にして実行してみます。
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の台数も設定可能です。
import { defineConfig } from '@playwright/test';
export default defineConfig({
// ~~~他の設定~~~
workers: process.env.CI ? 2 : undefined,
// ~~~他の設定~~~
});
テスト実行時のオプションでも指定できます。
npx playwright test --workers 4
デフォルトは、論理CPUコア数の半分とのことです。
Defaults to half of the number of logical CPU cores.
各テストファイル内でtest.describe.configureを使って設定
続いてはテストファイル内での設定方法です。
テストファイル内ではtest.describe.configure()
のmode
で設定します
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });
// ~~~テスト実装~~~
mode
は、parallel
/serial
/default
の3パターンがあります。
それぞれ実際に動かして試してみます。
parallel
まずはparallel
の場合です。こちらはその名の通り並行実行されます。
先ほどのテストファイルに追記して実行してみます。
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')
});
実行!
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)
並行実行されてますね!
fullyParallel
はfalse
に設定されていますが、その設定はこちらで上書きされます。
default
/serial
続いてはdefault
/serial
です。
いずれも直列実行されますが、default
は途中でテストが落ちてしまっても後続のテストが実行されるのに対し、serial
は途中でテストが落ちてしまうとテスト全体はそこで終了されます。
確認してみましょう。
まずはこれまでのテストファイル内でdefault
に設定して、実行してみます。
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')
});
実行!
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)
fullyParallel
がfalse
のときと同じく、直列実行されてますね。
それではtest2
を失敗するようにして実行してみましょう。
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')
});
実行!
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つのグループにくくるようなテストを書きました。
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のページに辿り着き、ここまでのような実験をして、
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 1
のtest.describe
内でmode
をdefault
に設定するに至りました。
ここまででは紹介していませんでしたが、test.describe
内でもmode
の設定はできます。
上記のように設定することで、test group 1
とtest group 2
は並行実行しつつ、test group 1
内のテストは直列実行することができます。
これでテストが最後まで通るようになり、事なきを得ました。
まずはちゃんと公式ドキュメントを読みましょう。大事。(再認識)
まとめ
今回はPlaywrightの並行実行について、実験を通して確認してみました。
これで並行実行を使いこなして、効率的なE2Eテストの自動化が実現できますね!
それでは!よいE2E自動テストライフを!
Discussion