nikkie-ftnextの日記

イベントレポートや読書メモを発信

ユーザベースの playtest2 が CI で examples を実行できるヒミツ 〜Python ポートでの再現を添えて〜

この記事は Uzabase Advent Calendar 2025 10日目です1
このカレンダー、ぜひ見ていただきたいですね。
どや俺の仲間すごいやろ!!」2


はじめに

七尾百合子さん、お誕生日 267日目 おめでとうございます! nikkieです。

ユーザベースが公開している実装から、私が「どやすごいやろ」と思ったポイントを紹介します。

目次

playtest2

ユーザベースでは Gauge を使って End-to-End のテストを書いています3

自然言語で仕様書としてテストを書けるのがいい感じです。

gauge.org

各種プログラミング言語に対応しています。
Python ではこんな感じ

ユーザベースは、Gauge でよく書く step をライブラリ化して公開しています4
GitHub - uzabase/playtest

このライブラリは第2世代5に突入しました!6

## /pingに対するテスト
* パス"/ping"に
* メソッド"GET"で
* メディアタイプ"application/json"で
* リクエストを送る
* レスポンスのステータスコードが
* 整数値の"200"である

これが手に馴染む感じだったので、私は今年 playtest2 を Python にポートしました(※勝手にやってる非公式な活動です)
https://pypi.org/project/playtest2/

playtest2 の examples は、実行できる例になっている!

ポートする中で、playtest2 の examples が興味を引きました。
https://github.com/uzabase/playtest2/tree/v0.0.9/examples

GitHub Actions でこの examples も実行しています。
https://github.com/uzabase/playtest2/blob/v0.0.9/.github/workflows/test.yaml#L22-L43

    steps:
      - uses: actions/checkout@v4
      - uses: sdkman/sdkman-action@b1f9b696c79148b66d3d3a06f7ea801820318d0f
        id: sdkman
      - name: Gauge install
        run: |
          curl -SsL https://downloads.gauge.org/stable | sh
      - name: Run tests
        run: |
          export JAVA_HOME=$(sdk home java 21.0.1-tem)
          mvn clean install -Dgpg.skip --no-transfer-progress
          cd examples/${{ matrix.environment }}
          mvn test --no-transfer-progress

あれ、mvn testしかしていない?
テスト対象の API は、どこで起動しているんでしょう?

BeforeSuite でテスト対象の API を起動

https://github.com/uzabase/playtest2/blob/v0.0.9/examples/simple-api-test/src/test/kotlin/Steps.kt#L16-L24 より抜粋

class Steps {
    @BeforeSuite
    fun beforeSuite() {
        App.startServer()
    }

BeforeSuiteフックを使い、テストスイートの実行前に、APIを起動しています7
https://docs.gauge.org/writing-specifications?os=macos&language=java&ide=vscode#execution-hooks

ふだんの開発ではテスト対象のAPIを別ターミナルで手動で立ち上げるので、フックを使ってコードからAPIを起動しているのが私には盲点でした

Python でやってみる ー デーモンスレッド!

playtest2-python でもこのたび導入しました。
FastAPI のアプリを @before_suite で起動します(uvicorn.run())。

uvicorn.run()をそのまま実行すると、起動したサーバがブロッキングしてテストが実行されませんでした。
ここでスレッドの出番かと膝を打ちました。
https://github.com/ftnext/playtest2-python/blob/4fa5d2257822288fe4f7c08dd68a62bfec1c7407/example/step_impl/execution_hooks.py#L16-L19

@before_suite
def start_app():
    server_thread = threading.Thread(target=uvicorn.run, args=(app,), kwargs={"port": 8000}, daemon=True)
    server_thread.start()

テストを実行した後、アプリをどう終了するかですが、これはuvicornを操作するのではなく、デーモンスレッドにすることで実現できました。
https://docs.python.org/ja/3/library/threading.html#thread-objects

スレッドには "デーモンスレッド (daemon thread)" であるというフラグを立てられます。
このフラグには、残っているスレッドがデーモンスレッドだけになった時に Python プログラム全体を終了させるという意味があります。

以上で、本家 playtest2 と同様に、example を実行できるようになりました!

終わりに

ユーザベースが公開するライブラリ playtest2 で、テスト対象 API を明示的に起動する代わりに BeforeSuite フックを使って起動していた話でした。
Python にポートしようとすると「無茶かもしれないが全部真似たい」と思うもので、それに突き動かされた結果、今回はデーモンスレッドという技術的な学びがありました。

P.S. なぜ Python ポート?

今年の DjangoCongress JP 2025(オンライン開催)がきっかけです。

先行発表の FastAPI アプリを私が主張する設計へと変更したのですが、前後で動きを変えていないことの確認に playtest2 を使いたくなり、ポートし始めました〜


  1. 過去にも参加しています。2023 2021
  2. 『ハイキュー‼︎』の北さんのセリフ(33巻)
  3. 最近エムスリーさんでも記事をお見かけして嬉しかったです
  4. ここ最近ではplaytestという、一般的なGauge Stepを定義したものをライブラリ化して利用しています。CIを改善し続けて見えてきた、高速で安定したCIを実現するためのTips
  5. E2E には Gauge や Playtest2、WireMock を利用していて、
  6. 第2世代の作者さまはこちらの発表の方です
  7. AfterSuiteフックで落としています。https://github.com/uzabase/playtest2/blob/v0.0.9/examples/simple-api-test/src/test/kotlin/Steps.kt#L32-L36