はじめに
本記事は、ACCESS Advent Calendar 2025の17日目の記事です。
こんにちは!QAエンジニアの @jyoppomu です。2025年12月現在の東京は冬本番で寒い日が続いているのでオープンしたばかりのRAKU SPA Station 武蔵小金井で身体を芯から温めながら執筆しました。
本記事では、筆者が普段テストコードを書く際に意識している「テストの前提条件の準備」と「テストの前処理」の区別について紹介します。あくまで、筆者個人の見解であり、自分の中で整理するために言語化したものです。
Zennのプロフィール設定の機能をテスト対象として、説明します。
テスト対象: Zennプロフィール設定
Zennのプロフィール設定画面(https://zenn.dev/settings/profile)には、以下の要素表示されています。
- 「表示名」入力欄
- 「自己紹介」入力欄
- 「GitHubユーザー名」入力欄
- 「Xユーザー名」入力欄
- 「プロフィール画像変更」ボタン
- 「更新」ボタン
ここで、「表示名」の項目は必須項目です。「表示名」入力欄が未入力の場合は、「更新」ボタンは無効化されます。
テストの前提条件の準備
「表示名」入力欄の入力の有無と「更新」ボタンの有効/無効の関係を確認するテストケースを考えます。例えば、「表示名」入力欄が未入力の場合、「更新」ボタンが無効化されていることを確認することを考えます。テスト実行手順を書くと、以下のようになります。
- 「表示名」入力欄を空にする
- 「更新」ボタンが無効化されていることを確認する
この手順でテストケースを実行するためには、「表示名」入力欄がテスト実行の前の時点で入力されている必要があります。このように、テスト実行のために必要な状態や条件を「テストの前提条件」と筆者は呼んでいます。また、この条件を満たすために必要な処理を「テストの前提条件の準備」と呼んでいます。この例では、「表示名」入力欄に任意の値を入力する操作が「テストの前提条件の準備」に該当します。
筆者は、この「テストの前提条件の準備」をひとつのテストケース内に明示的に記述するようにしています。そのテストケースに密接な関係がある処理であるためです。また、この「テストの前提条件の準備」は、テストコードを書く時には、テストケースの実行手順の一部として捉え、AAAパターンの「Arrange」やGherkin記法の「Given」に相当するものとし、記述します。
以下にPlaywrightを用いたテストコードの例を示します。ただし、locatorの定義は、あくまで例示目的であり、実際のZennの実装とは異なる可能性があります。
import { test, expect } from '@playwright/test';
test('表示名が未入力の場合、更新ボタンが無効化されていること', async ({ page }) => {
// テストの前提条件の準備
await test.step("arrange/given: プロフィールの表示名が入力済みであることを確認する", async () => {
await expect(page.getByRole("textbox", {name: "表示名"})).toHaveText("jyoppomu");
});
await test.step("act/when: 表示名入力欄を空にする", async () => {
await page.getByRole("textbox", {name: "表示名"}).fill("");
});
await test.step("assert/then: 更新ボタンが無効化されていることを確認する", async () => {
await expect(page.getByRole("button", {name: "更新"})).toBeDisabled();
});
});
テストの前処理
「テストの前処理」は、「テストの前提条件の準備」とは異なる概念として筆者は捉えています。「テストの前処理」は、テストケースを実行する前に毎回実行される共通の処理を指します。例えば、ログイン処理や特定の画面への遷移などが該当します。これらは、テスト実行のために必要な状態や条件のための準備ではありますが、各テストケースの実行手順とは密接には関係しない処理です。「そもそもこのテストケースを実行するために必要なこと整えること」という表現が適切かもしれません。
筆者は、この「テストの前処理」をひとつのテストケース内に明示的に記述することはせず、テストフレームワークの仕組みを用いて共通化しています。例えば、「ログイン処理」は以前紹介した「ログイン状態のセッションの共有」を用いています。リンク先の記事ではplaywright.config.tsでstorageStateを指定していますが、テストコード内で記述することもできます。
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: ".auth/user.json"
});
import { test } from './my-test';
test.describe("ログイン済みの状態でテストする", () => {
// テストケースの実装
});
また、「特定の画面への遷移」は、PlaywrightのFixturesをoverrideして、前処理を共通化するものよいと考えています。以下に例を示します。
import { test } from './my-test';
test.describe("ログイン済みの状態でテストする", () => {
test.describe("プロフィール設定", () => {
test.use({
page: async ({page}, use) => {
// プロフィール設定画面へ遷移
await page.goto("https://zenn.dev/settings/profile");
await use(page);
},
});
test("「表示名」入力欄のテスト" async ({ page }) => {
// テストケースの実装
});
test("「自己紹介」入力欄のテスト" async ({ page }) => {
// テストケースの実装
});
});
test.describe("カード情報の設定", () => {
test.use({
page: async ({page}, use) => {
// プロフィール設定画面へ遷移
await page.goto("https://zenn.dev/settings/billing");
await use(page);
},
});
test("「カード番号」入力欄のテスト" async ({ page }) => {
// テストケースの実装
});
test("「入稿期限」入力欄のテスト" async ({ page }) => {
// テストケースの実装
});
});
});
おわりに
本記事では、筆者が普段テストコードを書く際に意識している「テストの前提条件の準備」と「テストの前処理」の区別について紹介しました。この区別を明確にすることで、「このテストは何を確認するのか」や「テストケースを実行するために必要な状態や条件は何か」をより明確に理解できるように/読み取れるようになると考えています。本記事ではE2Eテストを例に説明しましたが、ユニットテストや統合テストなど他のテストレベルでも同様に適用できる考え方だと思います。「実行できるドキュメント」としてのテストコードをよりよいものにする工夫を今後も模索していきたいと思います。