以下の内容はhttps://tech.itandi.co.jp/entry/2026/02/20/114454より取得しました。


マルチプロダクトの品質を支えるテスト基盤「ITANDI QC」の紹介

こんにちは!イタンジ株式会社のフロントエンドチームに所属している西野です。 本記事では昨年末から始動したプロダクト共通の品質管理基盤「ITANDI QC」について紹介します。

ITANDI QCとは

イタンジではRSpecやデザインシステム内のコンポーネントテストは充実しています。しかしE2Eテストとリグレッションテストは不足しています。

イタンジのサービスは、別リポジトリで開発される複数のプロダクトで構成されています。 そのため、ユーザーシナリオを1つ完結させるためには複数のプロダクト間を遷移する必要があります。 各リポジトリでもローカル開発環境と検証環境は繋がっているため横断的なテストは可能でしたが、テストコードが各リポジトリに分散していることから、以下のような課題がありました。

  • 横断テストをどのリポジトリで管理すべきか判断しづらい
  • 1つのプロダクトのリポジトリにテストを集約すると、他プロダクトのテストコードや認証情報を持つこととなり責務が膨らむ

これらの課題を解決するために、どのプロダクトにも属さないE2Eテスト専用のリポジトリを新設し、持続可能な品質保証体制を作ることにしました。それが「ITANDI QC」です(QCはQuality Control:品質管理の略)。

デザインシステム「ITANDI BB UI」とそれを支えるStorybookの紹介」の記事でも紹介しましたが、イタンジには「ITANDI BB UI」というデザインシステムとそれをもとにしたコンポーネントライブラリが存在します。 このコンポーネントライブラリを前提としたE2Eテストを統一した基準で実装・運用するために、ITANDI QCの管理と基準策定はフロントエンドチームが行っています。

ディレクトリ構成と技術スタック

リポジトリ全体ではpnpm workspaceを採用しており、そのworkspaceの1つとしてitandi-e2eが存在します。 itandi-e2eの技術スタックは下記の通りです。

  • 言語: TypeScript
  • テストツール: Playwright
  • Linter / Formatter: Biome / ESLint

itandi-e2eのworkspace構造は以下の通りです。

e2e
├── package.json
├── playwright.config.ts
├── README.md
├── tests
│   ├── common
│   │   ├── fixtures
│   │   │   └── index.ts
│   │   ├── types
│   │   │   └── env.d.ts
│   │   └── utils
│   │       └── index.ts
│   ├── globalSetup.ts
│   └── products
│       ├── productA
│       │   ├── common
│       │   │   ├── fixtures
│       │   │   │   └── index.ts
│       │   │   └── utils
│       │   │       └── index.ts
│       │   ├── scenarios
│       │   │   ├── シナリオ1
│       │   │   │   └── index.spec.ts
│       │   │   ├── シナリオ2
│       │   │   │   └── index.spec.ts
│       │   │   └── シナリオ3
│       │   │       └── index.spec.ts
│       │   └── visual
│       │       ├── screenshot.spec.ts
│       │       └── views.ts
│       ├── productB
│       │   ├── common
│       │   │   ├── fixtures
│       │   │   │   └── index.ts
│       │   │   └── utils
│       │   │       └── index.ts
│       │   ├── scenarios
│       │   │   ├── シナリオ1
│       │   │   │   └── index.spec.ts
│       │   │   ├── シナリオ2
│       │   │   │   └── index.spec.ts
│       │   │   └── シナリオ3
│       │   │       └── index.spec.ts
│       │   └── visual
│       │       ├── screenshot.spec.ts
│       │       └── views.ts
│       └── productC
│           ├── common
│           │   ├── fixtures
│           │   │   └── index.ts
│           │   └── utils
│           │       └── index.ts
│           ├── scenarios
│           │   ├── シナリオ1
│           │   │   └── index.spec.ts
│           │   ├── シナリオ2
│           │   │   └── index.spec.ts
│           │   └── シナリオ3
│           │       └── index.spec.ts
│           └── visual
│               ├── screenshot.spec.ts
│               └── views.ts
└── tsconfig.json

各ディレクトリの詳細は下記の通りです。

  • tests/common
    • プロダクト共通のfixturesや型定義、便利関数など
  • tests/products/${product}/common
    • プロダクト固有のfixturesや型定義、便利関数など
  • tests/products/${product}/scenarios
    • 単一のプロダクトで完結するシナリオテストのコード
  • tests/products/${product}/visuals
    • 単一のプロダクトのVRTに関する処理
    • 具体的な差分検知の方法は後述します

横断テストについては現在対応方針を検討中です。

こだわりポイント

このテスト基盤を構築していく上でこだわったポイントがいくつかあるので紹介します。

テストの階層構造をlintで強制

社内ライブラリの1つにディレクトリ構造を強制するためのlintがあります。 それを使用して tests 配下で作成できるディレクトリの数、命名、ネストできるディレクトリの数、ファイル名などを制御しています。

認証状態の永続化

Playwrightでは下記のように tests/auth.setup.ts を作ることで .auth/user.json に認証状態を保存することができ、以降のtestを認証された状態で開始できます。

以下はサンプルコードです。

import { test as setup, expect } from '@playwright/test';
import path from 'path';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Username or email address').fill('username');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('https://example.com/');
  await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
  await page.context().storageState({ path: authFile });
});

イタンジのプロダクトは認証基盤が共通化されており、1つのログイン処理で複数のプロダクトにアクセスできます。 イタンジでは認証情報(storageState)を関数内で保存するようにし、呼び出し元ごとに別々の認証情報を保持するようにしました。 こうすることで各テストファイルで認証情報を使い回すことができ、testブロックごとに毎回ログイン処理を実行する必要なく、テスト実行時間を削減できます。

import type { BrowserContextOptions, Page } from "@playwright/test";
import { test as base } from "@playwright/test";

export function createPersistentAuthedTest(
   login: (page: Page) => Promise<void>
) {
    let storageState: BrowserContextOptions["storageState"];

    const persistentAuthedTest = base.extend({
        context: async ({ browser }, use) => {
            if (storageState === undefined) {
                const loginContext = await browser.newContext();

                const page = await loginContext.newPage();

                try {
                    await login(page);
                    storageState = await loginContext.storageState();
                } finally {
                    await page.close();
                    await loginContext.close();
                }
            }

            const context = await browser.newContext({ storageState });

            await use(context);
            await context.close();
        }
    });

    return { persistentAuthedTest };
}

コンポーネントを操作するためのnpm package

ITANDI BB UIのコンポーネントのうち、ButtonやInputなどHTML標準に近いコンポーネントは通常のPlaywrightのコード(例: page.getByRole("button", { name: "保存" })page.getByPlaceholder("年 / 月 / 日"))でもLocatorを取得できます。 しかしComboBoxやDatePickerなど複雑なDOM構造を持つコンポーネントの場合、内部の要素を正確に特定して操作するのは困難です。

そこで各コンポーネントを操作するためのnpm packageを実装しました。 下記のような関数が存在し、コンポーネントライブラリ側のバージョンと合わせて使用することで安全にコンポーネントを操作でき、コンポーネントの内部構造が変わってもテストコードを修正する必要がないようにしています。

export const getMultiComboBox = (
   page: Page,
   options?: {
        nth?: number;
        parent?: Page | Locator;
    }
) => {
    const parent = options?.parent ?? page;
    const selector = ".itandi-bb-ui__MultiComboBox__Input";
    const self =
        options?.nth !== undefined
            ? parent.locator(selector).nth(options.nth)
            : parent.locator(selector);

    const getInputs = async () => {
        const inputs = self.locator("../..").locator("input[hidden]").all();
        return inputs;
    };

    const getValue = async () => {
        const inputs = await getInputs();
        const values = await Promise.all(inputs.map((i) => i.inputValue()));
        return values;
    };

    const select = async (labels: string[]) => {
        for (const label of labels) {
            await self.fill(label);
            await self.press("Enter");
        }
        await self.blur();
    };

    return { getInputs, getValue, select, self };
};

// 使用例
const multiComboBox = getMultiComboBox(page);
await expect(multiComboBox.self).toBeVisible();
await multiComboBox.select(['オプション1', 'オプション2']);

CI

テストはGitHub Actionsの workflow_dispatch を使用して実行しています。 実行する際にBranch、テスト対象のプロダクト、前回の実行バージョン(VRT比較元)、今回の実行バージョンを入力して実行します。

テストを実行する過程でスクリーンショットを保存しておき、そのスクリーンショットを使ってVRTを行います。 VRTをしてレポートを出力するところまでが社内ライブラリになっています! VRTのレポートとPlaywrightのレポートはAmazon S3にアップロードされ、認証付きのCloudFrontから配信しています。 実行結果はSlackに流れるようになっており、VRTのレポートとPlaywrightのレポートのリンクが貼ってあり、テスト結果の詳細をすぐに確認できるようになっています。

テストが全てpassした時 テストがfailedになった時

今後の展望

テスト作成からPRレビューまでの一連の流れを効率化できないか検討しています。 playwright-cli などを使い、AIがDOMを確認してコードを修正できるのが理想だと思っています。

GitHub Copilot用に copilot-instructions.md でコーディング規約を管理しています。 今後は規約を整備しつつ、Page object models などの設計パターンも検討していきたいと考えています。

AIがコードを修正する際には @itandi/playwright の関数も適切に利用するよう調整していく予定です。

おわりに

現在はE2Eテストの管理が中心ですが、プロジェクト名を「ITANDI QC」としたのは、将来的に品質管理全般を担う基盤にするためです。

今後は負荷テストなどのコードもこのリポジトリに集約し、開発組織全体で効率的に品質向上に取り組める環境を構築していきます!




以上の内容はhttps://tech.itandi.co.jp/entry/2026/02/20/114454より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14