コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

GaugeとPlaywrightをGitHub Actionsで実行する際に工夫していること

こんにちは!プロダクト開発部の関根です。
飛行機好きの息子のために飛行機が見られるお出かけスポットやいい感じのYouTube動画を探す毎日です。

さて、コドモンではATDDでソフトウェアを開発しており、E2EテストのツールとしてGaugeやPlaywrightを利用しています。
今回は、E2EテストをCI基盤であるGitHub Actions上でも動作させるために工夫していることをいくつか紹介したいと思います。

CI環境におけるE2Eテストの流れ

GitHub Actionsを用いたE2Eテストの実行プロセスは以下の通りです。
Workflowが開始されると、最初にk8s環境へのデプロイ用コンテナイメージのビルドが行われます。続いて、Self-hosted Runnerを使用してk8sリソースを構築し、ヘルスチェックが完了するのを待ちます。その後、GaugeによってE2Eテストを実行し、テスト完了後にはk8sリソースを削除します。このプロセスはWorkflowの実行ごとに繰り返されます。

GitHub Actions上でのE2Eテスト実行フロー

このプロセスはWorkflowの実行ごとに繰り返されます。 k8sリソースの構築が動くので時間のかかるWorkflowとなっています。

実行環境とテストの構成

E2Eテストの実行環境は、以前に紹介したPull Request時のテスト環境自動作成と同様の方式で構築しています。

tech.codmon.com

コドモンでは主なアプリケーションとしてこのようなアプリケーションが稼働しており、

① [施設, 保護者向け] Webアプリケーションサーバ(PHP)
② [施設向け] Webフロントエンド
③ [保護者向け] モバイルアプリ
④ DBサーバ (MySQL) ⑤ セッション管理用Redis

加えて、マイクロサービス化しているサービス に関しては追加で

⑥ [施設, 保護者向け] Webアプリケーションサーバ
⑦ [施設向け] Webフロントエンド
⑧ DBサーバ(PostgreSQL)

をk8sに対してデプロイしています。

テストを実行する環境の構成図

なお、E2Eテストを実行するGitHub Actionsのself-hosted runnerも、DBマイグレーションを実行したりSecurity Groupによるアクセス元の制限をかけるために、同一のk8sクラスタ内の別Namespaceで実行してHTTP通信だけはインターネット経由となるよう設計しています。

テスト高速化のために

Playwrightを利用したE2Eテストは時間がかかるものです。テストの実行環境がCIのランナー含めてすべてコンテナであるため、イメージの改善やキャッシュの利用など一般的な高速化テクニックが有用であります。それぞれ解説していきます。

テスト実行イメージの改善

先述の通り、GitHub Actions上でE2Eテストを実行するためにself-hosted runnerを利用しています。
テスト実行の高速化のためにベースイメージに対して以下のような対応を行いました。

日本語フォントのインストールとロケール変更

Gaugeのspecが日本語で記載されており、 ubuntu/latest イメージにおいては日本語フォントがインストールされていないためにログ出力が文字化けしてしまいます。そのためIPAフォントをインストールし、併せてロケールも日本語へと変更しました。

Playwrightに必要なライブラリのインストール

Playwrightは起動時に自動的にChromium等のブラウザのバイナリをダウンロードしますが、それらを動作させるためには必要なライブラリ等がインストールされている必要があります。この作業は、PlaywrightのCLIを利用することで npx playwright install-deps chromium のようなコマンドにより簡単に実行できます。今回は、たまたまk8s上で動作するself-hosted runnerのオートスケール対応(参考)のためにベースイメージを summerwind/actions-runner:ubuntu-22.04 としており、そこにpython3が標準でインストールされていたためpythonでPlaywright CLIをインストールしました。

これらを組み合わせて、最終的にはこのようなDockerfileとなっています。

FROM summerwind/actions-runner:ubuntu-22.04

# 必要なものをインストール
RUN sudo apt update && sudo apt install -y \
    language-pack-ja \
    fontconfig \
    fonts-ipafont \
    && sudo rm -rf /var/lib/apt/lists/*

# 日本語設定
ENV LANG=ja_JP.UTF-8

# フォントインストール
RUN sudo fc-cache -fv && \
    fc-list | grep IPA


# Playwright CLIと依存ライブラリのインストール
RUN curl https://bootstrap.pypa.io/get-pip.py | python3 && \
    python3 -m pip install playwright && \
    playwright install-deps

Actionsでのキャッシュ

E2EはGauge JavaをKotlinから呼び出す形で実装されておりgradleのプラグインにより実行しています。gradleタスクとして実行されるということは、gradleのキャッシュを効かせることで不要な依存ライブラリのダウンロードを抑え、テスト全体の実行を早くすることができます。gradle公式のactionのキャッシュ機構は極めて便利で、特別な設定無しにbuild.gradleファイルの内容などをキーにキャッシュすることができるので利用しています。

なお、コンテナイメージのビルド時も同様にキャッシュを積極的に利用していますが、これも特別なことはしていないので割愛します。

テストの結果やログ取得の工夫

テストの実行の高速化も重要ですが、それと同じくらいにテスト失敗時の調査やログ残しも重要になってきます。一回一回のE2Eテストの実行に時間がかかるうえに実行環境を毎回構築しなおしている都合、失敗時にログ等からある程度問題の原因が特定できないと何度も再実行を繰り返して解消までの時間が伸びてしまいます。
ゆえに、テスト実行環境であるActions上で何が起きているのかを即座に確認できる方法として、コンソールへのログ出力とテスト結果レポートファイルのアウトプットを行っています。

Actions上のコンソールに出す内容

E2Eテストにおけるログ出力は、テストがPlaywright上でのブラウザ操作を中心としているために自ずとPlaywright関連のログを中心的に出すことになります。
なお、ログ出力がトラブルシューティングに有用な一方で、E2Eテストをローカルで実行するときやテストが安定して動いている場合などにおいてはログ自体が邪魔になる可能性もあるため、設定でオンオフ切り替えられるようにしておくのがよいでしょう。紹介するいくつかのログ出力も全てそのようにしています。

Playwrightのコンソールログ

ブラウザ操作によるE2Eテストでありがちなエラーとして、なにかしらの操作によるフロントエンドでの例外発生があげられます。UIのあるブラウザ操作であれば開発者ツールなどからログを見ることは容易いですが、CI環境においてはヘッドレスなブラウザを動かすことになるためブラウザを動かすテストの各ステップがハンドリングを行います。

Playwrightがコンソールへのログ出力時にフックする処理を宣言できるので、そこを利用して実装しています。また、logTypeに応じてログレベルと出力内容に微妙な違いを設けています。こうすることで、ブラウザ上で console.error("failed to render html") と出力すると、この処理を経由して最終的にはActions上のログとして [BrowserPage Error] failed to render html と表示されます。

// Page上でのログを標準エラーに出力する
val enablePlaywrightLog = System.getenv("playwright_browser_log")?.let { it.toBoolean() } ?: false
Logger.info("enablePlaywrightLog: $enablePlaywrightLog")
if (enablePlaywrightLog) {
    page.onConsoleMessage { consoleMessage ->
        val logType = consoleMessage.type()
        when (logType) {
            "error" -> {
                val message = "[BrowserPage Error] " + consoleMessage.text() + "\n" + consoleMessage.location()
                Logger.error(message)
            }

            "warning" -> {
                val message = "[BrowserPage Warning] " + consoleMessage.text()
                Logger.warning(message)
            }

            else -> {
                val message = "[BrowserPage Log] " + consoleMessage.text()
                Logger.info(message)
            }
        }
    }
}

このようなログが出て、エラーログをActionsに集約することができました

Playwrightのネットワークログ

同様に、WebフロントエンドとWeb APIの通信もありがちなエラーです。Web APIサーバがHTTP 5xxなどのエラーのステータスを返していることをトラッキングすることも問題の特定を容易にします。

これもまたPlaywrightがブラウザが送信したHTTPリクエストに対するレスポンス返却時にフックする処理を宣言できるので実装していきます。明確にエラーの場合だけをログに落としたいのでstatusが5xx系のものに限定してリクエストのURLとレスポンスのボディをログに出力します。

// Page上でのHTTPレスポンスのログを出力する
val enablePlaywrightHttp500Log = System.getenv("playwright_http_500_log")?.let { it.toBoolean() } ?: false
Logger.info("enablePlaywrightHttp500Log: $enablePlaywrightHttp500Log")
if (enablePlaywrightHttp500Log) {
    page.onResponse { response ->
        if (response.status() in 500..599) {
            val request = "Request: ${response.request().method()} ${response.url()}"
            val responseBody = "ResponseBody: ${response.text()}"
            val message = "[BrowserPage HTTP 500] Request: $request $responseBody"
            Logger.error(message)
        }
    }
}

このようなログが出て、通信状況もActionsから可視化できていい感じです。

セッション情報のデバッグ

Webアプリケーションを含むE2Eテストの、主にテストを作成したりCI環境上での動作を組み込んでいく際にありがちな問題が、ログインセッションの保持に関するものです。ローカルではうまく動作したのにホスト名や構成が異なるCIでうまく動作しないことが要因です。一般的なWebアプリケーションと同様にコドモンにおいてもCookieにセッション情報を保持しており、そのような際にはCookieは期待通りに焼かれるかを確認したくなります。

以下の例ではPlaywrightのPageインスタンスからCookieの値をすべて取得してログに出力しています。別のやり方としては、ログインのリクエストに対するレスポンスのSet-Cookieヘッダをログに出すのも有益かもしれません。

val cookies = page.context().cookies()
val enableSessionCookieLogAfterLogin =
    System.getenv("enable_session_cookie_log_after_login")?.let { it.toBoolean() } ?: false
if (enableSessionCookieLogAfterLogin) {
    println("Cookies after manager login: \n${cookies.joinToString("\n") { it.getFullInfo() }}")
}

fun Cookie.getFullInfo(): String = """
========================================
name: ${this.name}
value: ${this.value}
domain: ${this.domain}
path: ${this.path}
expires: ${this.expires}
httpOnly: ${this.httpOnly}
secure: ${this.secure}
sameSite: ${this.sameSite}
========================================
""".trimIndent()

テスト結果のレポート

最後に紹介するのは、テストレポートの出力についてです。
E2Eテストの証跡や失敗時の調査を目的として、テストレポートやPlaywrightによるブラウザ操作の動画をActionsのoutputとして保存し、一定期間中は参照することができるようにしています。

docs.github.com

今回のE2Eテストの実行時に出力しているものは以下の2つです。

html_report

Gaugeのhtml-reportプラグインが出力するhtmlレポートです。
yaml-reportやjson-reportのプラグインを利用してActionsの画面上にレンダリングすることも考えましたが時間の都合でやれていません...

Playwrightのブラウザ操作録画

ログが便利だといいつつも、ブラウザ操作をテストしている限り、UIの操作が記録に残せるに越したことはありません。動画が残っていることで操作とログを照らし合わせながらトラブルシューティングできます。
これはPlaywrightのBrowserContextを生成する際に動画撮影のオプションを宣言することで、任意のディレクトリに対してブラウザ操作の動画を保存することができます。

val enableRecordVideo = System.getenv("record_video")?.let { it.toBoolean() } ?: false
Logger.info("enableRecordVideo: $enableRecordVideo")
val browserContextOptions = Browser.NewContextOptions().apply {
    if (enableRecordVideo) {
        this.setRecordVideoDir(Paths.get("./screenshots/")).setRecordVideoSize(1280, 720)
    }
}
val browserContext = browser.newContext(browserContextOptions)

その他

今回のテストでは組み込まれていませんが、テスト自体を並列実行するという手法や、

tech.codmon.com

Gauge Javaを利用している場合はPlaywrightをスレッドごとに動かすというアプローチもあります。

jsoizo.hatenablog.com

これらも高速化の上では極めて有効な手段です。ぜひお読みください。

まとめ

今回はコドモンで行っているGaugeとPlaywrightで実行しているE2Eテストの環境や高速化、ログ取得、テストレポートについて説明いたしました。安定的にテストが通らない場合があるなど、まだまだ課題もあり一部のサービスで利用するにどどまっていますが、冪等な環境でテストができるという点は非常に優れたものであり、安定してテストが通る状況の適用範囲を広げられたらと考えています。