画面リプレースの回帰確認に、昔のテスト項目書を使う 〜 Qase × Playwright で、手動テストを 8 割減らした 〜

画面リプレースの回帰確認のため、旧システムのテスト項目書を Qase に入れ、Playwright の E2E テストと結び付けた取り組みと、やってみて分かったことをまとめます。

sasaki・October 27, 2026

はじめに

古い業務システムの画面を、機能はそのままに Angular から React へ書き換えています。全 22 画面です。

この記事では、旧システムのテスト項目書を Qase に入れ、Playwright の E2E テストとケース ID で結び付けた取り組みと、やってみて分かったことをまとめます。

背景

フロントエンドは技術の移り変わりが速く、社内で標準とするテストのやり方を、バックエンドほどには固めていませんでした。このシステムの画面も、Excel のテスト項目書(約 850 件)にもとづく手動テストで確認してきました。

今回は機能を変えずに画面だけを書き換えるので、旧システムと同じように動けば正しい、と言えます。何を確かめればよいかは、項目書にそのまま書かれています。ただ、これだけの件数を画面が変わるたびに手で流すのは現実的ではありません。そこで、項目書を Qase に入れ、自動化できるものを E2E テストにしてケースと結び付けました。

結果:手動テストが 8 割減った

項目書のケースの 8 割ほどを E2E にしました。リリース前に手で流す必要があるのは、残りの 2 割弱です。

E2E は PR ごとに CI で流れ、6 並列で約 14 分で終わります。結果は月に 1 回、Qase の Test Run としてケースごとに記録されます。

E2E の結果を自動で記録した Qase の Test Run

移行の漏れを早く拾えた

昔の項目書で確かめたことで、書き換えの途中で漏れていたものを早い段階で拾えました。

  • 8 つのフォームで、文字数の上限や必須チェックといった入力検証が漏れていた
  • 詳細画面から一部の項目を編集する機能が、まだつながっていなかった

どちらも、新しい画面だけを見てテストを書いていたら、気づきにくかったと思います。

旧画面にあって新画面に無い機能が見つかったときは、issue に切り出して「新画面にも作るか、機能をやめてケースを消すか」を判断しています。判断の結果は、Qase のケースとテストコードの両方に反映します。

見た目や使い勝手は、引き続き手動で確認する

手で確認するケースは 2 割弱になりましたが、手動テストがなくなったわけではありません。

E2E で分かるのは、操作したときに期待どおりの結果になるかどうかです。レイアウトの崩れや文字の見切れといった見た目の問題は、テストが通っていても見つけられません。こうした部分は、これまでどおり手動で確認しています。

やってみて分かったこと

テストとケースは、コードの中で結ぶ

playwright-qase-reporter の qase() を使うと、テストとケースの結び付けはコードの中で完結します。Qase 側で設定することはありません。

test(qase([812, 822], '基本属性を編集で、今の値が入った編集ダイアログが開く'), async ({ page, testData }) => {
  // …
});

1 本のテストに複数の ID を付けられるのも便利でした。昔の項目書には、同じ確認が別の画面の項目として重複して載っていることがあります。それをテスト 1 本でまとめて済ませられます。

冪等性の確保と、ステップのある流れ

1 本のテストで確かめる操作は 1 つだけにして、それより前のステップは API で用意しています。また、テストに使うデータはそのテストの中で作り、終わったら消します。もともと環境にあるデータや、ほかのテストが作ったデータには頼りません。

たとえば「顧客を登録し、契約を登録し、定期メンテナンスありにすると、メンテナンスの予定が作られる」という流れです。このテストで確かめたいのは最後の「定期メンテナンスありにすると、予定が作られる」なので、顧客と契約の登録は API で済ませます。(例は、実際の業務を一般的な内容に置き換えています)

test(qase([12, 118], '開始日を入れて定期メンテナンスありにすると、有効なメンテナンス種別の数だけ予定が作られる'), async ({ page, testData, uniqueMarker }) => {
  // 前提は API で作る
  const customer = await testData.createCustomer({ name: `${uniqueMarker} 顧客` });
  const contractId = await testData.createContract({ name: `${uniqueMarker} 契約`, customer });
 
  // 確かめたい操作だけを画面で行う
  const contractPage = new ContractPage(page);
  await contractPage.goto(contractId);
  await contractPage.setStartDate('2026-01-15');
  await contractPage.enableMaintenance();
  await testData.adoptMaintenancesOf(contractId); // アプリが自動で作った予定も、片付けの対象にする
 
  await contractPage.openTab('メンテナンス');
  // 期待値(メンテナンス種別の数)は、テナントの設定を API で読んで決める
});

テストデータは、次のように扱っています。

  • 前提のデータは、テストの最初に API で作る
  • データの名前に、そのテスト専用の識別子(uniqueMarker)を入れる。画面で検索するときもこの識別子で絞るので、ほかのデータが混ざらない
  • テストが終わったら、作ったデータを API で消す。画面の操作で作られたデータも、登録 API の応答から id を拾って消す

消すときは、作った順と逆に消します。子のデータが残っていると、親を消せないためです。

前提を DB に直接入れず API で作るので、検索インデックスへの反映もアプリ本来の流れで行われます。ただし反映は非同期なので、検索 API に出てきたのを確かめてから画面を開きます。

このプロジェクトの fixture は、一部を省くと次のような形です。テストは testData を受け取るだけで、片付けは意識しません。

export const test = base.extend<{ testData: TestDataApi; uniqueMarker: string }>({
  testData: async ({ request }, use) => {
    const testData = new TestDataApi(request);
    await use(testData);
    await testData.dispose(); // 作ったものを逆順に消す
  },
  uniqueMarker: async ({}, use, testInfo) => {
    await use(`e2e${Date.now().toString(36)}${testInfo.workerIndex}`);
  }
});

どこからが確かめたい操作かは、テストごとに決まります。たとえば「登録 → 編集 → 削除」では、3 つとも確かめたい操作で、Qase でも別々のケースです。そこで 3 本のテストに分けます。

  • 登録のテスト:画面で登録する
  • 編集のテスト:登録は API で済ませてから、画面で編集する
  • 削除のテスト:登録は API で済ませてから、画面で削除する

ステップを順に画面で流す、1 本の長いシナリオにする書き方もあります(Playwright なら describe.serial でテストを順につなげられます)。それをしなかったのは、次の理由からです。

  • 途中で 1 つ失敗すると、後ろのテストがすべて落ちる
  • テストが順番に依存するので、並列に分けられない
  • 前提を画面で作るより、API で作るほうが速い
  • Qase の 1 ケースが 1 本のテストに対応するので、結果が読みやすい

この形にしたことで、テストは順番に依存しなくなりました。6 並列にしても安定して流れますし、CI で失敗したテストを 1 回だけ再実行する設定も、安心して使えます。その代わり、通しで流したときにだけ出る不具合は拾いにくくなります。そこはトレードオフとして受け入れています。

Qase への送信は、定期実行の 1 回だけにする

PR のたびに結果を送ると、Qase に Test Run が積み上がって読めなくなります。そこで、送信は環境変数で切り替え、月 1 回の定期実行のときだけ送るようにしました。

  • ローカルと PR では送らない(既定で off)
  • 定期実行のときだけ、並列をやめて 1 ジョブで流す。並列のまま送ると、Test Run が分かれてしまうため

進め方:量の多い作業は Claude Code に任せた

700 件を超えるテストを人の手だけで書くのは、さすがに大変です。そこで、実装には Claude Code を使いました。項目書の Qase への投入、E2E の土台づくり、機能ごとのテストの移行を、機能ごとに PR を分けて進めています。

項目書を先に Qase に入れておいたのが、ここで効きました。ケース数が分母になるので、「どのケースが済んで、どれが残っているか」を 1 件ずつ追えます。どこまで終わったかが見えるので、AI が書いたテストのレビューも進めやすくなりました。どの機能を新画面にも作るか、やめるかといった仕様の判断は、人が行いました。

振り返り

大量のテストを、ほぼ網羅する形で効率よく自動化できたのは良かったです。

一方で、操作性やレイアウトは、やはり人が実際に触って確かめる必要があると感じました。たとえば、検索ボタンを押せば検索できることは E2E で確かめられます。ただ、そのボタンの位置が画面ごとに違う、といった使い勝手の不揃いには、E2E では気づけません。

古いテスト項目書が残っているプロジェクトなら、同じやり方で回帰確認を仕組みにできると思います。