Playwright で作成したテストのパフォーマンスを改善する

# はじめに

本記事では、 E2E テストツールの Playwright(C#/MSTest) で作成したテストのパフォーマンス改善を行った際のポイントをご紹介します。

# まとめ

- 原則ヘッドレスモードでテストを実行する
- テストケースごとにブラウザーを閉じる処理を挟む
- 上記の改善により、テスト実行時間を約 1/4 に短縮

# Playwright とは？

Playwright は Web アプリケーションのテストを自動化するテストフレームワークです。
JavaScript/TypeScript, Java, Python, C# での開発が可能です。 Chrome, Edge, Firefox, Safari 等のブラウザーに対応しています。
導入コストが低く、コード自動生成やトレース保存機能を利用できるのが特徴です。

https://playwright.dev/

# 背景

UI 部品に関するテストを Playwright で実行できるように整備し、作成したテストを都度実行した際は正常にテストが実行できていました。
ところが、いざ全件実行してみると10数件実行した時点でマシンの電源が落ちてしまう、テストの全件実行に 2 時間程度かかるといった問題がありました。
対象のテストは約 200 件で、それぞれ 3 つのブラウザーで実行していました。
単純にテストケースと実行対象のブラウザーの種類が多いにしても、自動実行が厳しいためパフォーマンス改善することになりました。

テスト対象の UI 部品が .NET ベースであるため、本記事では C# 版の Playwright を利用して MSTest でテストを実装した場合について説明しています。

# 実行環境のバージョン

- マシンスペック
    - OS：Windows 10
    - RAM：8GB
    - CPUコア数: 4
    - ディスク容量：239GB
- Visual Studio 2022
- .NET 6
- OSSバージョン
    - Microsoft.Playwright 1.27.1
    - Microsoft.Playwright.MSTest 1.27.1

※リファクタリング前後の比較のため、 Playwright と .NET のバージョンはテスト作成時点 (2022/12) のままです。

# テストコードの構成

## 改善前のテストコード

Playwright が提供する部品をラップしてテストコードから呼び出すよう構成しました。
複数ブラウザーでテスト実行しやすいよう TestBrowser.cs を実装しています。

``` c#:TestBrowser.cs
using Microsoft.Playwright;

namespace TestClient
{
    public class TestBrowser
    {
        private IPlaywright playwright;

        private string browserName;

        private IBrowser? browser;

        private IBrowserContext? browserContext;

        public TestBrowser(IPlaywright playwright, string browserName)
        {
            this.playwright = playwright;
            this.browserName = browserName;
        }

        public async Task<IPage> GetPageAsync()
        {
            browser = await GetBrowserAsync();
            browserContext = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
            return await browserContext.NewPageAsync();
        }

        private async Task<IBrowser> GetBrowserAsync()
        {
            return browserName switch
            {
                "Edge" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, Channel = "msedge" }),
                "FireFox" => await playwright.Firefox.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false }),
                "Chrome" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, Channel = "chrome" }),
                _ => throw new NotSupportedException($"{browserName}はテストに対応していません。")
            };
        }
    }
}
```

MSTest のテストコードからブラウザーの種類ごとに TestBrowser のインスタンスを生成してテストに利用しています。
`TestBrowser.GetPageAsync()` で指定したブラウザー（Chrome, Edge, Firefox）の [Browser](https://playwright.dev/dotnet/docs/api/class-browser) のインスタンスを生成し、 [Page](https://playwright.dev/dotnet/docs/api/class-page) を返します。
テストクラスは Playwright が用意しているベースクラス ([PageTest](https://playwright.dev/dotnet/docs/test-runners#base-mstest-classes-for-playwright)) を継承します。

``` c#:SampleTest.cs
using Microsoft.Playwright.MSTest;
using System.Text.RegularExpressions;

namespace TestClient
{
    [TestClass]
    public class SampleTest : PageTest
    {
        private static readonly string[] BrowserTypes = { "Edge", "FireFox", "Chrome" };

        [TestMethod]
        public async Task Test1()
        {
            // ブラウザーの種類ごとにテストロジックを呼び出し
            foreach (string currentBrowser in BrowserTypes)
            {
                var browser = new TestBrowser(Playwright, currentBrowser);
                var page = await browser.GetPageAsync();

                // テストに応じた画面操作等
                await page.GotoAsync("https://playwright.dev");
                await Expect(page).ToHaveTitleAsync(new Regex("Playwright"));
            }
        }
    }
}
```

## 改善後のテストコード

主に次の2点を行いました。

- **ヘッドレスモードの有効化**
テスト作成時には画面の状態や、対象ブラウザーの起動を確認するためにヘッドレスモードを無効にしていました。テストコードの実装完了後は基本的に目視での確認は不要なのでヘッドレスモードを有効化しました。

- **ブラウザーを閉じる処理の追加**
ヘッドレスモードを無効化した際、ブラウザーを閉じる処理を入れずにテストを実行しているとテストごとにブラウザーウィンドウが開くことを確認できます。明示的にブラウザーを閉じない限り、実行対象のテストが全て終了するまでブラウザーは開いたままです。これにより、テストを連続実行するとメモリ枯渇を起こし、マシンの電源が落ちる等していました。そのため、テストケースごとにブラウザーを閉じる処理 ([BrowserContext.CloseAsync()](https://playwright.dev/dotnet/docs/api/class-browsercontext#browser-context-close), [Browser.CloseAsync()](https://playwright.dev/dotnet/docs/api/class-browser#browser-close)) を呼び出すことでメモリを解放するよう修正しました。

:::note info
ヘッドレスモードを有効化している場合でも、ブラウザーを閉じる処理によってテスト実行時のパフォーマンスを改善できます。
:::

``` diff:TestBrowser.cs（追加・変更部分のみ）
+ public async Task CloseAsync()
+ {
+     if (browserContext != null && browser != null)
+     {
+         await browserContext.CloseAsync();
+         await browser.CloseAsync();
+     }
+ }

private async Task<IBrowser> GetBrowserAsync()
{
    return browserName switch
    {
-        "Edge" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, Channel = "msedge" }),
-        "FireFox" => await playwright.Firefox.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false }),
-        "Chrome" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, Channel = "chrome" }),
+        "Edge" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true, Channel = "msedge" }),
+        "FireFox" => await playwright.Firefox.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true }),
+        "Chrome" => await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true, Channel = "chrome" }),
        _ => throw new NotSupportedException($"{browserName}はテストに対応していません。")
    };
}
```

``` diff:SampleTest.cs
using Microsoft.Playwright.MSTest;
using System.Text.RegularExpressions;

namespace TestClient
{
    [TestClass]
    public class SampleTest : PageTest
    {
        private static readonly string[] BrowserTypes = { "Edge", "FireFox", "Chrome" };

        [TestMethod]
        public async Task Test1()
        {
            foreach (string currentBrowser in BrowserTypes)
            {
                var browser = new TestBrowser(Playwright, currentBrowser);
                var page = await browser.GetPageAsync();

                // テストに応じた画面操作等
                await page.GotoAsync("https://playwright.dev");
                await Expect(page).ToHaveTitleAsync(new Regex("Playwright"));
                
+               // ブラウザーを閉じる処理を追加
+               await browser.CloseAsync();
            }
        }
    }
}
```

:::note info
ブラウザーを閉じる処理の呼び出しについて

ブラウザーを閉じる処理の呼び出しを強制するには以下の 2 通りの方法が考えられますが、実行時間が長くなります（今回のテスト環境では実行時間が約 1.1 ～ 1.3 倍になることが確認できました）
    - `finally` ブロックにブラウザーを閉じる処理を実装
    - TestBrowser に `IAsyncDisposable` インターフェースを実装し、 `DisposeAsync` メソッドにブラウザーを閉じる処理を実装
:::

:::note info
テストの並列実行について

本記事ではテスト用アプリケーションの構成の都合上利用できませんが、 Playwright では MSTest のクラス単位での並列実行をサポートしています。CPU やメモリに余裕がある状態であれば、並列実行によりテストの高速化が期待できます。
https://playwright.dev/dotnet/docs/test-runners#running-mstest-tests-in-parallel
:::

# パフォーマンス測定

リファクタリング前後で対象のテストの実行時間と以下の指標を測定しました。

- Processor(_Total)\\% Processor Time: CPU 使用率
- Memory\Available MBytes: 利用可能な物理メモリの容量(メガバイト単位)
- Memory\Pages/sec: 1 秒当たりのページング（物理メモリとディスクの間のデータのロードや退避）の回数
- Memory\Committed Bytes: 仮想メモリの使用量
- Memory\Commit Limit: 仮想メモリの容量
- Memory\\% Committed Bytes In Use: 仮想メモリの使用率
- PhysicalDisk(_Total)\Current Disk Queue Length: ディスクに残っている要求の数
- PhysicalDisk(_Total)\\% Disk Time: ディスクドライブが読み取り/書き込み要求を処理していてビジー状態にあった経過時間の割合
- PhysicalDisk(_Total)\Disk Transfers/sec: ディスク上の読み取り/書き込み操作の速度

参考：https://nets-tip.com/?p=72

なお、テスト実行中にマシンがダウンしないよう複数のテスト群に分けてテストを実行しています。

## コード改善前
  
- 実行時間合計：7162.9s(=約2h)

|                                                | 最大値      | 最小値      | 平均値      |
| ---------------------------------------------- | ----------- | ----------- | ----------- |
| Processor(_Total)\\% Processor Time             | 99.792      | 6.044       | 55.964      |
| Memory\Available MBytes                        | 2965.000    | 12.000      | 410.859     |
| Memory\Pages/sec                               | 84320.573   | 412.433     | 40225.670   |
| Memory\Committed Bytes                         | 31957917696 | 13047775232 | 21522944400 |
| Memory\Commit Limit                            | 32613867520 | 28122624000 | 31017248058 |
| Memory\\% Committed Bytes In Use                | 98.191      | 40.457      | 69.463      |
| PhysicalDisk(_Total)\Current Disk Queue Length | 252.000     | 0.000       | 72.984      |
| PhysicalDisk(_Total)\\% Disk Time               | 36360.940   | 4.952       | 8168.940    |
| PhysicalDisk(_Total)\Disk Transfers/sec        | 6919.074    | 94.588      | 2581.076    |

## コード改善後

- 実行時間合計：1954.5s(=約32.6min)

|                                                | 最大値      | 最小値      | 平均値      |
| ---------------------------------------------- | ----------- | ----------- | ----------- |
| Processor(_Total)\% Processor Time             | 96.112      | 7.820       | 54.518      |
| Memory\Available MBytes                        | 2354        | 575         | 1556.898    |
| Memory\Pages/sec                               | 34261.264   | 12.725      | 2778.065    |
| Memory\Committed Bytes                         | 15504801792 | 12681674752 | 13598116383 |
| Memory\Commit Limit                            | 25071128576 | 25071128576 | 25071128576 |
| Memory\% Committed Bytes In Use                | 61.843      | 50.583      | 54.238      |
| PhysicalDisk(_Total)\Current Disk Queue Length | 9           | 0           | 0.218       |
| PhysicalDisk(_Total)\% Disk Time               | 1850.350    | 2.437       | 102.008     |
| PhysicalDisk(_Total)\Disk Transfers/sec        | 1464.263    | 13.058      | 219.972     |

## パフォーマンスの変化

### 実行時間

コードの改善前後で、テスト実行時間を約 1/4 に短縮することができました。なお、上記の測定値には表れていませんが、テスト全件を連続実行できるようになりました。

### メモリ

メモリ関連の測定値を見ると、Memory\Available MBytes の平均値が増加、Memory\\% Committed Bytes In Use の平均値が減少しており、メモリの空きを一定以上確保した状態でテストが継続できるようになったことが分かります。

### ディスク

ディスク関連の測定値では、 PhysicalDisk(_Total)\Current Disk Queue Length, PhysicalDisk(_Total)\\% Disk Time, PhysicalDisk(_Total)\Disk Transfers の平均値が大幅に減少しています。Memory\Pages/sec の平均値が減少していることから、ディスクへのアクセス要求が減少し、ディスクの負荷が軽減されていると考えられます。

### CPU 使用率

Processor(_Total)\\% Processor Time の平均値はコード改善後わずかに低くなっていますが、コードの改善前後で CPU は同程度で稼働できているといえます。ディスクアクセスの減少により、 CPU への負荷が多少軽減していると考えられます。

# おわりに

本記事では、 Playwright で作成したテストのパフォーマンス改善方法について説明しました。自動テストの実装に慣れていないためにつまずいたポイントかと思いますが、大量のテストを自動化する際の参考になれば幸いです。

## We Are Hiring!

https://www.biprogy.com/recruit/recruiting/