📝

ノーコードプラットフォームからPlaywrightへテストを移行した話。パート2:ノーコードからMarkdownへ

に公開

はじめに

パート1では、なぜノーコードのテストプラットフォームを離れたのか、そしてどのように移行計画を立てたのかについて解説しました。今回は実践編です。ノーコードのテストケースを、QAやPdM(プロダクトマネージャー)が実際に読んで検証できるプレーンなMarkdownへと変換します。変換を行うスキルパイプライン、Arrange-Act-Assert(AAA)フォーマット、テスト用パスワードをGitに含めないための工夫、そして次回同じことをする前に知っておきたかったことについて解説します。

なぜ直接Playwrightではなく、Markdownなのか

前回の記事で触れた通り、Markdown層を挟む理由は、テストケースとその実装を切り離す(デカップリングする)ためです。人間が読める1つのテストケースで異なるプラットフォームやバージョンを対象にできますし、PdMやQAはPlaywrightのコードを一切開くことなくロジックをレビューできます。

当初のアイデアでは、MarkdownとPlaywrightのコードを一発の処理(シングルパス)で同時に生成しようと考えていました。しかし、2つの理由から断念しました。
まず、両方の処理を同時にやらせると、モデルの精度がどちらのタスクでも目に見えて低下したためです。さらに、MarkdownからPlaywrightへの変換全体の実行速度が極めて遅いことも分かりました。レビューの締め切り(実際には主要なレビュアーが時間を割ける限られた時間枠)に間に合わなくなるほど遅かったのです。

作業を分割したことで、嬉しい副作用もありました。Playwrightのコードを後から作成する一方で、QAはすぐにMarkdownのレビューを開始できるようになったのです。

その結果、最終的な計画は以下のようになりました。

  1. すべてのノーコードのテストケースを、10個ずつのバッチに分けてMarkdownに移行する。
  2. QAがプルリクエスト(PR)で各バッチをレビューする。開発者が軽微な修正を行ってマージする。
  3. その後、適切なタイミングで、それらのMarkdownファイルをPlaywrightのテストに変換する(次回の記事で解説)。

パイプライン

移行作業全体を通して、大がかりなツールは使いませんでした。使用したのは、Opus 4.8を搭載したプレーンなClaude Codeスキル、VS Code、そしてノーコードプラットフォームのAPIを呼び出すいくつかのNode.jsスクリプトだけです。

以下の図を見てみましょう。詳細を解説します。

作業は1つの巨大なプロンプト(メガプロンプト)に頼るのではなく、スコープを絞った小さな「スキル」に分割されています。各スキルは1つのことだけを実行し、次のスキルへと処理を引き渡します。

  • nocode-to-raw — エクスポートスクリプトを呼び出し、生のノーコードのステップツリーを .nocode-export/<Category>/<Number>_<Name>/ にダンプして、プラットフォームのデータのローカルコピーを作成します。
  • raw-to-aaa — 実際の変換処理。生のエクスポートデータを読み込み、ステップツリーをArrange-Act-Assert(AAA)形式のMarkdownに書き換えます。

これらを分割したのには実用的な理由があります。APIのエクスポートは確実(再現可能)かつ低コストなため、一度実行すれば終わり(fire-and-forget)にできます。一方で、変換処理は私が実際に試行錯誤(イテレーション)を繰り返す部分です。これらを切り離しておくことで、ノーコードのAPIに何度も負荷をかけることなく、改善したスキルを使って何度でも変換を再実行できます。また、生のエクスポートデータとの差分(diff)を取ることで、プラットフォームから何が提供され、それに対してモデルがどう処理したのかを正確に把握できます。

AAA Markdownフォーマット

この段階において最も重要なのはフォーマットです。以下に、実際の(一部省略した)例を示します。各テストケースは、tests/<top-dir>/<feature>/ 配下に配置される、<NNNN>_<title>.md という名前の単一のMarkdownファイルです。

# 0055: メールでサインイン - 登録済みのメールアドレスとパスワードでのサインインに成功する

tags: level3, auth, form

arrange:
- 共通ステップ: ログアウト状態でサインインページにアクセスする (tenantId: example)

act:
- "Email" 入力欄に "user@example.com" と入力する
- "Password" 入力欄に ${E2E_PASSWORD_1} と入力する
- "メールでサインイン" ボタンをクリックする

assert:
- URLが ${baseURL}/view/home に一致することを確認する

これがコードのダンプではなく、テスト計画書のように読める理由は、何よりも**「3つのフェーズに分け、どこに何を配置するかを厳格にルール化する」**という原則にあります。

arrange(準備)は、セットアップとナビゲーション(共通ステップ、goto、クッキーやストレージのクリア、変数)を担当します。

act(実行)は、ユーザーの操作のみ(入力、クリック、選択、スクロール)を担当します。

assert(検証)は、検証のみ(URLの一致、表示状態、テキストの確認)を担当します。

モデルには明確な分類テーブルが与えられているため、たとえば「表示されるまで待つ」といったステップは(初期状態を待つという意味で)arrange に分類され、決して assert に入ることはありません。

残りの作業はクリーンアップです。ノーコードのエクスポートデータには、人間のレビュアーにとって意味のないマークアップが大量に含まれています。そのため、スキルによってそれらをそぎ落とし、セレクターや実装の詳細を含まない、プレーンで読みやすいステップへと正規化します。

タグ

tags: の行は、ノーコードプラットフォームから引き継いだメタデータです。主に次の3つのタイプを保持しています:level(ケースの重要度)、feature areaauthposts などの機能領域)、typeformnavigation などの種類)。

Markdown内では単にカンマ区切りのテキストにすぎず、QAが頭の中でフィルタリングするにはこれで十分です。次の段階では、これらがPlaywrightのタグに変換され、CLIやGitHub Actionsから特定のサブセット(例:リリース前のスモークテスト一式のみなど)を実行できるようになります。

共通ステップ

arrange は、ほぼ常に shared step:(共通ステップ)の行から始まります。これらはそれぞれ、ステップの説明から派生したキャメルケース(camelCase)の名前を持つ shared-steps/ 内の関数と1対1でマッピングされます。

AAAの行(Markdown) 関数名
access the sign-in page while logged out accessSignInPageInLoggedOutState
log in as a normal user loginAsNormalUser

Markdown段階における最大のメリットは、ログインやナビゲーション、後処理(ティアダウン)といった処理が、すべてのケースにコピペされた15行の低レベルなステップではなく、1行の読みやすい記述に凝縮される点です。レビュアーは「通常のユーザーとしてログインする」という記述を見て、そのまま先へ進むことができます。操作の共通化は重複排除にも役立ちます。2つのノーコードケースが同じ15行のステップで始まっている場合、それらは両方とも同じ1行の記述に変換されるため、重複していることが一目で明らかになります(詳細は後述)。

sopsによるパスワードの管理

ここが苦労したポイントの1つでした。このノーコードプラットフォームはパスワードをプレーンテキストで保存しており、APIからのエクスポートも同様の形式で行われます。これらは使い捨てのテスト用パスワードであり、他の社内ツールからもすでにプレーンテキストで確認できるものだったため、厳重にロックをかけること自体が目的ではありませんでした。ただ、プレーンテキストのままリポジトリに存在させたくなかったですし、かといって何十個ものパスワードを手動でシークレットマネージャーに登録するのも避けたかったのです。

sops は、この2つのアプローチのちょうど中間に位置する解決策です。.env ファイルを暗号化するため安全にコミットでき、開発者はパスワードマネージャーに保管されている age キーを使って復号できます。本当のデメリットは、キーをローテーション(更新)しても過去の漏洩をなかったことにはできない点です。古いバージョンのパスワードは Git の履歴に永久に残ります。しかし、前述の脅威モデル(想定されるリスク)を考慮した結果、私はシンプルさを優先しました。

その結果、パスワードのワークフローは以下のようになりました。

  • スキルが移行中にカスタムスクリプトを実行してパスワードを抽出し、それらを .env に登録した上で、.env.example に空のレコードを追加します。
  • 開発者が暗号化コマンドを実行すると .enc.env が更新され、Markdownファイルと一緒にプッシュされます。
  • 自身のローカル環境にプレーンテキストの .env を用意したい別の開発者は、age キーを使って復号コマンドを実行するだけです。

登録スクリプトは小規模なものです。パスワードのリテラルを渡すと、次に使用可能な変数名を返してくれます。

$ npx tsx register-password.ts "p@ssw0rd"
E2E_PASSWORD_3

このスクリプトは .env.env.example をスキャンし、次のインデックス(E2E_PASSWORD_3)を割り当て、その値を .env に書き込み、.env.example に空のプレースホルダーを作成します。その後、Markdownからは ${E2E_PASSWORD_3} として参照されるだけです。各呼び出しはアトミックに一意の名前を確保するため、複数のエージェントで同時に移行処理を実行しても、2つのエージェントが E2E_PASSWORD_1 を奪い合うようなことはありませんでした。

sops側の設定は、.env を1つの age 受信者(recipient)に紐付ける単一のルールのみです。

# .sops.yaml
creation_rules:
  - path_regex: .env
    input_type: dotenv
    age: age1aa0...9c9as5   # パブリック受信者。秘密鍵はパスワードマネージャーに保管

エージェント自身の ignore リストから .env.enc.env を除外しているため、Claude側には ${E2E_PASSWORD_N} というプレースホルダーしか見えません。プレーンテキストのファイルを読み取ることも、暗号化されたファイルを解読することもできません。

結果として、以下の3種類のファイルが存在することになります。

# .env.example - コミット対象
E2E_PASSWORD_1=
# .env - git-ignore および agent-ignore 対象
E2E_PASSWORD_1=real-password
# .enc.env - コミット対象
E2E_PASSWORD_1=abcdertsasklfnl

ただし、この抽出スクリプトは完璧ではなく、特に一括移行(バルク移行)の際には課題もありました。すでに登録済みのパスワードを常に検出できるとは限らなかったため、PRレビューの際に紛れ込んだ生のパスワードがないか注意を払い、取り除く必要がありました。この点については、プロンプトのファインチューニングをより精緻に行うことで軽減できた可能性があります。

git worktree によるスケールアップ

コンテキストウィンドウの制限を考慮し、すべてを一度に移行しようとしてはいけません。私がこれをスケールさせた方法は、専用の「一括処理(bulk)スキル」の作成でした。たとえば、未移行のケースを50個のバッチとして取得し、それを10個ずつのチャンクに分割します。そしてチャンクごとに独立した git worktree を立ち上げ、各 worktree に対してサブエージェントを並列で実行します。各サブエージェントが10個のケースを移行し、レビュアーとなるサブエージェントがそのバッチを検証した上で、独自のPRを作成します。ここで重要なディテールが worktree です。これにより、並列実行される各エージェントに隔離されたチェックアウト環境が提供されるため、お互いのファイルを上書きしてしまう心配がありません。まずは小さく(10個から)始めて出力の品質を確認し、問題がなければ50個、200個と規模を拡大していくのが良いでしょう。

得られた教訓

重複テストの削減

移行完了後、実際のテストケースを振り返る時間ができ、テストの数を約10%削減することができました。Markdownファイルを読んでいると、同じフローが2回テストされていたり、あるテストが別のテストのフローをほぼすべてカバーしていたりすることに気づきやすくなります。ノーコードツールのUI上では事実上見えなくなっていた要素が、Markdownを読むと非常に明白になるのです。

実用的な注意点として、単に重複を削除して終わりにしてはいけません。私は、削除されたケースが現在どのケースによってカバーされているかを記録する小さな deduped-cases.json を保持しています。変換スキルは、再生成を行う前にこのファイルを確認します。そうしないと、後から一括で再実行した際に、意図的に削除したテストが何事もなかったかのように復活してしまいます。

スキルを常に最新の状態に保つ

フィードバックに基づいてスキルを常に更新し、移行の進捗を分析し、よくある問題をスキルファイルやカスタムのリンターのようなスクリプトにコード化(ルール化)しておきましょう。プラットフォーム固有の不要なノイズを取り除く正規化ルールも、最初はすべて単発のレビューコメントから始まり、それを後にスキルへと昇格させたものでした。

結論

以上が前半戦のすべてです。すべてのノーコードのケースは、人間が実際に読める数行の Arrange-Act-Assert 形式のMarkdownになり、テスト用パスワードはそのすぐそばに暗号化されて保管されています。QAはPR上のバッチ処理を順次進め、その都度承認(サインオフ)を出してくれました。現在、このMarkdownが私たちの「信頼できる唯一の情報源(Source of Truth)」となっています。

次に取り組むPlaywrightは、考えられる実装方法の1つに過ぎません。それこそが Part 3 のテーマです。これらのMarkdownファイルを、実行可能なPlaywrightのテストへと変換するステップについて解説します。

コミューン株式会社

Discussion