カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

頑張りすぎず、PlaywrightのCI実行時間を短縮した話

カミナシのソフトウェアエンジニアisanaです。 カミナシレポートの開発に携わっています。

私たちのチームでは、Webアプリケーションの品質担保のため、Playwrightを用いたブラウザテストを実装し、GitHub Actionsで実行しています。しかし、このCIプロセスにおいていくつかの課題がありました。

他方、ソフトウェア開発においては日々寄せられるVoCに対応したり、新機能の開発を行うなかで、負債や課題を上手くハンドリングしていく必要があります。

本稿では、CIプロセスにおける課題をコスパよく解決するための改善策と、その過程で遭遇した「ハマったポイント」について、具体的な設定例を交えながらご紹介します。

PlaywrightやGitHub Actionsを利用している開発者の方々にとって、少しでも参考になれば幸いです。

前提となる環境

本稿で紹介する事例は、以下の環境を前提としています。

  • テストフレームワーク: Playwright v1.52.0
  • CI/CDプラットフォーム: GitHub Actions
  • パッケージマネージャー: pnpm

直面していた課題とその影響

冒頭でも触れましたが、私たちのチームが抱えていたCIの課題は主に以下の2点です。

タイムアウトするまで実行しつづけているjobがあった

何らかの不具合によってPlaywrightのテストがハングしたとき、CIのjobがfailせずにタイムアウトしていました。

GitHub Actionsのデフォルトタイムアウトは360分となっており、jobがハングしていることに気づかないとCIの待ち時間が発生するだけでなく、GitHub Actionsのリソースも無駄に消費してしまい、不要なコストを支払っていました。

CIの実行時間が長い(約15分)

テストが全て正常に完了する場合でも、全体の実行に15分程度かかっていました。

これは、プルリクエストを出してからフィードバックを得るまでの待ち時間としては長く、開発体験を損ねる一因となっていました。

改善にあたり重視したポイント

これらの課題を解決するにあたり、以下の2点を特に重視しました。

1. CIにおけるテスト実行時間の目標設定

やみくもに高速化を目指すのではなく、明確な目標を持つことが重要だと考えました。

そこで参考にしたのが、DevOps Research and Assessment (DORA) が提唱する指標です。

DORAによると、「自動テストの待ち時間は10分以内が望ましい」とされています。

The tests should not take more than a few minutes to run, with an upper limit of about 10 minutes according to DORA’s research . 出典: DORA | Capabilities: Continuous Integration

この「10分以内」という具体的な基準があったことで、速度改善のゴールが明確になり、取り組みやすくなりました。

2. クリティカルパスの改善

CIパイプラインは複数のjobで構成されており、並列実行も組み合わせて構築されていることが一般的です。

全体の待ち時間を短縮するためには、全てのjobを均等に少しずつ高速化するよりも、最も時間のかかっているjob(クリティカルパス)を特定し、集中的に改善する方が効果的です。

今回は並列実行しているブラウザテストの中で、最も実行に時間がかかっているクリティカパスに対して改善を行いました。

具体的に取り組んだ改善策

上記の課題と重視したポイントを踏まえ、以下の改善策を実施しました。

1. jobのタイムアウト設定

まず、テストがハングアップした際にCIリソースを無駄に消費し続けないように、jobに適切なタイムアウト値を設定しました。GitHub Actionsでは、ワークフローファイル内で timeout-minutes を指定することで、jobの最大実行時間を制御できます。

# .github/workflows/ci.yml
jobs:
  playwright-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 20 # 例えば20分でタイムアウトさせる
    steps:
      # ... テスト実行ステップ ...

これにより、万が一テストがハングしても、設定した時間で自動的にjobが停止し、早期に問題を検知できるようになりました。

2. キャッシュの活用

一般論として、CI/CDの最適化にあたりまず検討するべきポイントはキャッシュの利用です。

CI/CDボトルネックの把握とその先へ | ドクセル

a. node_modules のキャッシュ

node.jsのセットアップに利用する定番アクションであるsetup-nodeはnode_modulesのcacheを隠蔽してくれるのでそれを利用します。

下記のようにwith.cachepnpmを指定するだけでOKです。

# .github/workflows/ci.yml (一部抜粋)
jobs:
  playwright-test:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
    - uses: actions/checkout@v4.2.2
    - name: Setup pnpm
      uses: pnpm/action-setup@v4.1.0
      with:
        version: 10
    - uses: actions/setup-node@v4.4.0
        with:
        node-version-file: 'package.json'
        cache: 'pnpm'

pnpm installの実行時間は、全体の実行時間に対して微々たるものですが、上述の通り極めて簡単に利用できるため、設定しておいて損はないです。

b. Playwrightが依存しているブラウザのキャッシュ

Playwrightはテスト実行時に対応するブラウザ(Chromium, Firefox, WebKitなど)をダウンロード・インストールします。これもキャッシュすることでjobの実行時間を短縮できます。

ここで、キャッシュの話に入る前に、Playwrightの依存関係についておさらいします。

Playwrightには、Playwright自身の実行に必要な依存ライブラリとテストの実行に利用する依存ブラウザがあります。

依存ライブラリはパッケージマネージャを利用して所定のパスにインストールされます。

一方、依存ブラウザは~/.cache/ms-playwrightにインストールされます。

Managing browser binaries | Playwright

このディレクトリをキャッシュすることで、依存ブラウザをインストールするstepをスキップすることができます。

依存ブラウザをキャッシュしつつ、依存ライブラリをインストールするアプローチとして、本稿では以下2つのコマンドを組み合せるアプローチをご紹介します。

  • playwright install --with-deps:Playwrightの依存ライブラリと依存ブラウザをインストールする
  • playwright install install-deps : Playwrightの依存ライブラリのみインストールする

Install Browsers | Playwright

このアプローチでは下記のように、依存ブラウザがキャッシュヒットしたときは依存ライブラリのみをインストールし、依存ブラウザがcacheヒットしなかったときは依存ライブラリと依存ブラウザの両方をインストールするようになっています。

jobs:
  playwright-test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
    # ....node, pnpm, node_modulesのsetup....
    - name: Get installed Playwright version # playwrightのバージョンを控える
      id: playwright-version
      run: echo "version=$(pnpm exec playwright --version | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+')" >> $GITHUB_OUTPUT
    - name: Cache Playwright Browsers # Playwrightのバージョンをkeyに依存ブラウザをcache
      uses: actions/cache@v4.2.3
      id: playwright-cache
      with:
        path: ~/.cache/ms-playwright
        key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}'
        restore-keys: ${{ runner.os }}-playwright-
    - name: Install Playwright Browsers # cacheがない時は依存ブラウザと依存ライブラリを両方インストール
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pnpm exec playwright install --with-deps
    - name: Install Playwright system dependencies # cacheがある時は依存ライブラリのみインストール
      if: steps.playwright-cache.outputs.cache-hit == 'true'
      run: pnpm exec playwright install-deps
    # ....テスト実行....

4. クリティカルパスにのみlarge runnerを利用

今回改善を行ったプロジェクトでは、Desktop Chrome、Mobile Chrome (Android)、Mobile Safari (iOS) の3つのブラウザ環境に対するテストを並列実行していました。

この中でも特にMobile Safari でのテストが最も時間がかかっており、全体のクリティカルパスとなっていました。

キャッシュを活用しただけではMobile Safariでのテストにおいて、目標の10分を切ることが出来なかったため、更なる改善が必要でした。

ここで、Reactのレンダリングを最適化することでブラウザテストを高速化するアプローチを検討しました。

しかし、今回の対応のスコープ内でレンダリングのパフォーマンス改善を実施するのは難易度が高いと判断し諦めることにしました。

そこで、Mobile Safariのテストのみ、標準のランナーよりも高性能な GitHub-hosted large runner (Ubuntu 8-core) を利用することにしました。

About larger runners - GitHub Docs

# .github/workflows/ci.yml (一部抜粋)
jobs:
  test-mobile-safari:
    runs-on: ubuntu-8-core # mobile safariのテストのみlarge runnerを指定
    timeout-minutes: 20
    steps:
      # ... WebKit用のテスト実行ステップ ...

  test-chrome:
    runs-on: ubuntu-latest # Chromeは標準ランナー
    timeout-minutes: 20
    steps:
      # ... Chrome用のテスト実行ステップ ...

他のブラウザテストは標準ランナーのままとし、クリティカルパスとなっている部分に限定して高性能なリソースを割り当てることで、コストを抑えつつ全体の実行時間短縮を図りました。

尚、GitHub-hosted large runnerを利用する際は、上記のようなjobの設定に加えて、所属するorganizationへの登録が必要になります。

Managing larger runners - GitHub Docs

ハマったポイントとその解決策

large runnerが起動せず、jobがスタートしない

large runnerをorganizationに追加し、上記のように設定したにもかかわらず、該当のjob (test-mobile-safari) がキューに入ったまま実行されない状態に陥りました。

Requested labels: ubuntu-8-core
Job defined at: xxxxxxxx
Waiting for a runner to pick up this job...

原因を調査したところ、organizationに登録したGitHub-hosted runnerの設定項目Maximum Job Concurrencyの値が「1」に設定されていたためでした。

これは、organizationに登録したrunnerを同時に実行できるjob数の設定です。

あるブランチのjobで登録したrunnerを実行しているとき、別のブランチで同じrunnerを指定したjobは全て待機状態になっていました。

Maximum Job Concurrencyの値を、最大値である1000に変更したことでこの問題を解決することができました。

この設定項目については、GitHubの以下のドキュメントで解説されています。

Managing larger runners - Configuring autoscaling for larger runners (公式ドキュメント)

もしlarge runnerの利用で同様の問題に直面した場合は、この設定を確認してみてください。

まとめ

本稿では、PlaywrightとGitHub Actionsを用いたブラウザテストのCIにおいて、サクッとできる改修で待ち時間を10分に収めるようにした事例をご紹介しました。

  • jobのタイムアウト設定をすることでjobのタイムアウトによる無駄なリソース消費を抑えることが出来た
  • node_modulesPlaywrightブラウザのキャッシュを利用し、large runnerをクリティカルパスに投入するすることでブラウザテストの待ち時間を約15分から7~9分以内に短縮できた

Playwrightによるブラウザテストを根本的に早くするための作業時間を確保するのは難しいですが、タイムアウト、キャッシュ、スケールアップをクリティカルパスに対してうまく活用することでコスパ良く改善することができます。

ちょっとした改善でも開発体験を良くすることができるので、こういった積み重ねを継続していきたいです。

PlaywrightやGitHub Actionsを利用してCIの課題に直面している方にとって、本稿が少しでもお役に立てれば幸いです。

最後に宣伝です📣

カミナシではソフトウェアエンジニアを募集しています。

herp.careers

herp.careers

プロダクト開発の傍ら、生産性の改善や負債の返済にも妥協なく取り組みたい方、是非一緒に働きましょう!

参考文献