T-CREATOR

Playwright × TypeScript で実現する型安全テスト自動化

Playwright × TypeScript で実現する型安全テスト自動化

JavaScript での E2E テストを運用していく中で、「テストコードが複雑になるにつれて保守が困難になる」「実行時エラーでテストが失敗する」「チームメンバー間でテストコードの理解に差が生まれる」といった課題に直面したことはありませんか?

これらの課題を根本的に解決するのが、TypeScript の型システムを活用したテスト自動化です。Playwright は TypeScript をネイティブでサポートしており、型安全性の恩恵を最大限に活用できます。

本記事では、JavaScript から TypeScript への移行がテスト自動化にどのような革新をもたらすのか、実際のコード例とともに詳しく解説していきます。型安全なテストコードを書くことで、開発効率と品質の両方を大幅に向上させることができるでしょう。

TypeScript がテスト自動化にもたらす価値

TypeScript をテスト自動化に導入することで得られる価値は、単なる「型チェック」以上の大きなメリットがあります。実際の開発現場で感じられる具体的な価値を見ていきましょう。

型チェックによるテストコードの品質向上

JavaScript でのテスト開発では、実行時まで発見できないエラーが多く存在します。TypeScript の型チェックにより、これらの問題をコンパイル時に検出できます。

typescript// JavaScript での問題例
// 実行時までエラーに気付かない
test('ログイン機能のテスト', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#username', 'testuser');
  await page.fill('#pasword', 'password123'); // タイポエラー
  await page.click('#login-btn');
});

// TypeScript での解決例
interface LoginForm {
  username: string;
  password: string;
  submitButton: string;
}

const loginSelectors: LoginForm = {
  username: '#username',
  password: '#password', // タイポがあれば型エラーで検出
  submitButton: '#login-btn',
};

test('型安全なログイン機能のテスト', async ({ page }) => {
  await page.goto('/login');
  await page.fill(loginSelectors.username, 'testuser');
  await page.fill(loginSelectors.password, 'password123');
  await page.click(loginSelectors.submitButton);
});

このように、TypeScript の型システムにより、セレクタの誤り、メソッドの呼び出しミス、プロパティの存在チェックなどを事前に検出できます。

IDE のサポートによる開発効率化

TypeScript を使用することで、VS Code などの IDE が提供する強力な開発支援機能を活用できます。

typescript// 型定義による IntelliSense の恩恵
interface TestUser {
  id: number;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'guest';
}

class UserTestHelper {
  async createUser(userData: TestUser): Promise<void> {
    // IDE が userData のプロパティを自動補完
    await this.page.fill('#email', userData.email);
    await this.page.fill('#name', userData.name);
    await this.page.selectOption('#role', userData.role);
  }

  async verifyUserProfile(
    expectedUser: TestUser
  ): Promise<void> {
    // 型安全な検証
    await expect(
      this.page.locator('#user-email')
    ).toHaveText(expectedUser.email);
    await expect(
      this.page.locator('#user-name')
    ).toHaveText(expectedUser.name);
  }
}

IDE のサポートにより、自動補完、型情報の表示、リファクタリング支援が利用でき、開発速度が大幅に向上します。

リファクタリング時の安全性確保

大規模なテストスイートでは、リファクタリング時の影響範囲を把握することが困難です。TypeScript の型システムが、変更の影響を静的に検証してくれます。

typescript// インターフェースの変更例
interface ApiResponse {
  data: UserData[];
  meta: {
    total: number;
    page: number;
    // pageSize: number; // この属性を削除
    limit: number; // 名前を変更
  };
}

// 型エラーにより、影響を受けるテストコードが即座に判明
test('API レスポンスの検証', async ({ request }) => {
  const response = await request.get('/api/users');
  const json: ApiResponse = await response.json();

  // コンパイル時に型エラーが発生
  // expect(json.meta.pageSize).toBe(10); // エラー: プロパティが存在しない
  expect(json.meta.limit).toBe(10); // 正しい属性名に修正が必要
});

このように、インターフェースの変更が全テストコードに与える影響を即座に把握でき、安全なリファクタリングが可能になります。

チーム開発での保守性向上

TypeScript により、テストコードが自己文書化され、チームメンバー間でのコードの理解が深まります。

typescript// 型定義により、テストの意図が明確になる
interface CheckoutTestData {
  products: Array<{
    id: string;
    name: string;
    price: number;
    quantity: number;
  }>;
  shippingAddress: {
    street: string;
    city: string;
    zipCode: string;
  };
  paymentMethod: 'credit_card' | 'paypal' | 'bank_transfer';
  expectedTotal: number;
}

class CheckoutTestScenario {
  constructor(private testData: CheckoutTestData) {}

  async executeTest(page: Page): Promise<void> {
    // テストデータの型により、必要な操作が明確
    await this.addProductsToCart(page);
    await this.fillShippingAddress(page);
    await this.selectPaymentMethod(page);
    await this.verifyTotal(page);
  }

  private async addProductsToCart(
    page: Page
  ): Promise<void> {
    for (const product of this.testData.products) {
      await page.goto(`/product/${product.id}`);
      await page.fill(
        '[data-testid="quantity"]',
        product.quantity.toString()
      );
      await page.click('[data-testid="add-to-cart"]');
    }
  }
}

型定義により、テストの構造、必要なデータ、実行フローが明確になり、新しいチームメンバーでも容易にテストコードを理解できます。

Playwright × TypeScript の強力な連携

Playwright は TypeScript をネイティブサポートしており、その組み合わせにより従来のテストツールでは実現できない強力な機能を提供します。

ネイティブ TypeScript サポートの恩恵

Playwright は最初から TypeScript での利用を想定して設計されており、追加設定なしで TypeScript の恩恵を受けられます。

typescript// 型定義ファイルの自動生成
// playwright.config.ts
import {
  defineConfig,
  devices,
  PlaywrightTestConfig,
} from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30000,
  expect: {
    timeout: 5000,
  },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }],
  ],
  use: {
    actionTimeout: 0,
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
}) satisfies PlaywrightTestConfig;

satisfies キーワードを使用することで、設定オブジェクトの型安全性を保ちながら、型推論も活用できます。

型定義による API の安全性

Playwright の全ての API が型定義されており、誤った使用法をコンパイル時に検出できます。

typescript// Locator の型安全な使用
import { test, expect, Locator } from '@playwright/test';

class SearchComponent {
  private readonly searchInput: Locator;
  private readonly searchButton: Locator;
  private readonly resultsContainer: Locator;

  constructor(private page: Page) {
    this.searchInput = page.locator(
      '[data-testid="search-input"]'
    );
    this.searchButton = page.locator(
      '[data-testid="search-button"]'
    );
    this.resultsContainer = page.locator(
      '[data-testid="search-results"]'
    );
  }

  async search(query: string): Promise<void> {
    await this.searchInput.fill(query);
    await this.searchButton.click();
    await this.resultsContainer.waitFor({
      state: 'visible',
    });
  }

  async getResults(): Promise<string[]> {
    const resultElements = await this.resultsContainer
      .locator('.result-item')
      .all();
    return Promise.all(
      resultElements.map((element) => element.textContent())
    ).then((texts) =>
      texts.filter((text): text is string => text !== null)
    );
  }

  async getResultCount(): Promise<number> {
    return await this.resultsContainer
      .locator('.result-item')
      .count();
  }
}

test('検索機能の型安全なテスト', async ({ page }) => {
  const searchComponent = new SearchComponent(page);

  await page.goto('/search');
  await searchComponent.search('TypeScript');

  const results = await searchComponent.getResults();
  const count = await searchComponent.getResultCount();

  expect(results).toHaveLength(count);
  expect(
    results.every((result) =>
      result.toLowerCase().includes('typescript')
    )
  ).toBe(true);
});

カスタム型定義でのテスト拡張

プロジェクト固有の型定義を作成することで、より表現力豊かなテストコードが書けます。

typescript// types/test-types.ts
export interface TestEnvironment {
  baseUrl: string;
  apiUrl: string;
  credentials: {
    admin: UserCredentials;
    user: UserCredentials;
  };
}

export interface UserCredentials {
  email: string;
  password: string;
}

export interface PageComponent {
  selector: string;
  waitForVisible?: boolean;
  timeout?: number;
}

export interface FormField {
  label: string;
  selector: string;
  type:
    | 'text'
    | 'email'
    | 'password'
    | 'select'
    | 'checkbox';
  required?: boolean;
  validation?: (value: string) => boolean;
}

// カスタム型を活用したテストヘルパー
class FormTestHelper {
  constructor(private page: Page) {}

  async fillForm(
    fields: FormField[],
    data: Record<string, string>
  ): Promise<void> {
    for (const field of fields) {
      const value = data[field.label];

      if (field.required && !value) {
        throw new Error(
          `Required field "${field.label}" is missing`
        );
      }

      if (
        value &&
        field.validation &&
        !field.validation(value)
      ) {
        throw new Error(
          `Invalid value for field "${field.label}": ${value}`
        );
      }

      switch (field.type) {
        case 'text':
        case 'email':
        case 'password':
          await this.page.fill(field.selector, value);
          break;
        case 'select':
          await this.page.selectOption(
            field.selector,
            value
          );
          break;
        case 'checkbox':
          if (value === 'true') {
            await this.page.check(field.selector);
          }
          break;
      }
    }
  }

  async validateFormErrors(
    expectedErrors: Record<string, string>
  ): Promise<void> {
    for (const [fieldName, expectedError] of Object.entries(
      expectedErrors
    )) {
      const errorSelector = `[data-testid="${fieldName}-error"]`;
      await expect(
        this.page.locator(errorSelector)
      ).toHaveText(expectedError);
    }
  }
}

型推論を活用したテスト作成

TypeScript の型推論機能により、明示的な型注針を減らしながら型安全性を保てます。

typescript// 型推論を活用したテストデータ生成
const createTestUser = (
  overrides: Partial<TestUser> = {}
) => ({
  id: Math.floor(Math.random() * 1000),
  email: 'test@example.com',
  name: 'Test User',
  role: 'user' as const,
  ...overrides,
});

// 型推論により、戻り値の型が自動的に決定される
const adminUser = createTestUser({ role: 'admin' });
const guestUser = createTestUser({ role: 'guest' });

// 条件分岐を型で表現
type TestBrowser = 'chromium' | 'firefox' | 'webkit';

const getBrowserConfig = (browser: TestBrowser) => {
  switch (browser) {
    case 'chromium':
      return { ...devices['Desktop Chrome'] };
    case 'firefox':
      return { ...devices['Desktop Firefox'] };
    case 'webkit':
      return { ...devices['Desktop Safari'] };
  }
};

// 型ガードを使用した安全な値チェック
const isValidEmail = (value: string): value is string => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
};

test('型推論を活用したテスト', async ({ page }) => {
  const user = createTestUser({
    email: 'admin@example.com',
  });

  if (isValidEmail(user.email)) {
    await page.goto('/login');
    await page.fill('#email', user.email);
    // user.email は string 型として扱われる
  }
});

実践的な型安全テスト設計パターン

実際のプロジェクトで活用できる、型安全性を最大限に活かしたテスト設計パターンをご紹介します。

Page Object Model の TypeScript 実装

Page Object Model を TypeScript で実装することで、保守性と再利用性の高いテストコードが作成できます。

typescript// pages/base-page.ts
export abstract class BasePage {
  protected constructor(protected page: Page) {}

  async goto(path: string): Promise<void> {
    await this.page.goto(path);
  }

  async waitForPageLoad(): Promise<void> {
    await this.page.waitForLoadState('networkidle');
  }

  protected async waitForElement(
    selector: string,
    timeout = 5000
  ): Promise<Locator> {
    const element = this.page.locator(selector);
    await element.waitFor({ state: 'visible', timeout });
    return element;
  }
}

// pages/login-page.ts
interface LoginCredentials {
  email: string;
  password: string;
}

interface LoginPageElements {
  emailInput: string;
  passwordInput: string;
  submitButton: string;
  errorMessage: string;
  forgotPasswordLink: string;
}

export class LoginPage extends BasePage {
  private readonly elements: LoginPageElements = {
    emailInput: '[data-testid="email-input"]',
    passwordInput: '[data-testid="password-input"]',
    submitButton: '[data-testid="login-button"]',
    errorMessage: '[data-testid="error-message"]',
    forgotPasswordLink:
      '[data-testid="forgot-password-link"]',
  };

  constructor(page: Page) {
    super(page);
  }

  async login(
    credentials: LoginCredentials
  ): Promise<void> {
    await this.page.fill(
      this.elements.emailInput,
      credentials.email
    );
    await this.page.fill(
      this.elements.passwordInput,
      credentials.password
    );
    await this.page.click(this.elements.submitButton);
  }

  async getErrorMessage(): Promise<string | null> {
    try {
      const errorElement = await this.waitForElement(
        this.elements.errorMessage
      );
      return await errorElement.textContent();
    } catch {
      return null;
    }
  }

  async isLoginButtonEnabled(): Promise<boolean> {
    const button = this.page.locator(
      this.elements.submitButton
    );
    return await button.isEnabled();
  }

  async clickForgotPassword(): Promise<void> {
    await this.page.click(this.elements.forgotPasswordLink);
  }
}

// pages/dashboard-page.ts
interface DashboardData {
  username: string;
  notifications: number;
  recentActivity: Array<{
    type: 'login' | 'update' | 'create';
    timestamp: string;
    description: string;
  }>;
}

export class DashboardPage extends BasePage {
  private readonly selectors = {
    welcomeMessage: '[data-testid="welcome-message"]',
    notificationBadge: '[data-testid="notification-badge"]',
    activityList: '[data-testid="activity-list"]',
    activityItem: '[data-testid="activity-item"]',
  } as const;

  constructor(page: Page) {
    super(page);
  }

  async getDashboardData(): Promise<DashboardData> {
    const welcomeText = await this.page
      .locator(this.selectors.welcomeMessage)
      .textContent();
    const username =
      welcomeText?.replace('Welcome, ', '') || '';

    const notificationText = await this.page
      .locator(this.selectors.notificationBadge)
      .textContent();
    const notifications = parseInt(
      notificationText || '0',
      10
    );

    const activityItems = await this.page
      .locator(this.selectors.activityItem)
      .all();
    const recentActivity = await Promise.all(
      activityItems.map(async (item) => {
        const type = (await item.getAttribute(
          'data-activity-type'
        )) as DashboardData['recentActivity'][0]['type'];
        const timestamp =
          (await item
            .locator('.timestamp')
            .textContent()) || '';
        const description =
          (await item
            .locator('.description')
            .textContent()) || '';
        return { type, timestamp, description };
      })
    );

    return { username, notifications, recentActivity };
  }

  async verifyWelcomeMessage(
    expectedUsername: string
  ): Promise<void> {
    await expect(
      this.page.locator(this.selectors.welcomeMessage)
    ).toHaveText(`Welcome, ${expectedUsername}`);
  }
}

カスタムフィクスチャの型定義

Playwright のフィクスチャ機能を TypeScript で拡張し、テスト間でのデータ共有を型安全に行えます。

typescript// fixtures/user-fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { DashboardPage } from '../pages/dashboard-page';

interface UserRole {
  email: string;
  password: string;
  role: 'admin' | 'user' | 'guest';
}

interface TestFixtures {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  adminUser: UserRole;
  regularUser: UserRole;
  authenticatedPage: Page;
}

export const test = base.extend<TestFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },

  adminUser: async ({}, use) => {
    await use({
      email: 'admin@example.com',
      password: 'admin123',
      role: 'admin',
    });
  },

  regularUser: async ({}, use) => {
    await use({
      email: 'user@example.com',
      password: 'user123',
      role: 'user',
    });
  },

  authenticatedPage: async (
    { page, loginPage, regularUser },
    use
  ) => {
    await page.goto('/login');
    await loginPage.login(regularUser);
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

export { expect } from '@playwright/test';

この型安全なフィクスチャを使用したテスト例:

typescript// tests/dashboard.spec.ts
import { test, expect } from '../fixtures/user-fixtures';

test.describe('ダッシュボード機能', () => {
  test('管理者ユーザーでのダッシュボード表示', async ({
    page,
    loginPage,
    dashboardPage,
    adminUser,
  }) => {
    await page.goto('/login');
    await loginPage.login(adminUser);

    await page.waitForURL('/dashboard');

    const dashboardData =
      await dashboardPage.getDashboardData();
    expect(dashboardData.username).toBe('Admin User');
    expect(
      dashboardData.notifications
    ).toBeGreaterThanOrEqual(0);
    expect(dashboardData.recentActivity).toBeInstanceOf(
      Array
    );
  });

  test('認証済みページでの操作', async ({
    authenticatedPage,
    dashboardPage,
  }) => {
    // 既に認証済みのページが提供される
    const dashboardData =
      await dashboardPage.getDashboardData();
    expect(dashboardData.username).toBe('Regular User');
  });
});

テストデータの型安全な管理

テストデータを型安全に管理し、データの整合性を保つパターンです。

typescript// data/test-data-types.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  category: 'electronics' | 'clothing' | 'books' | 'home';
  inStock: boolean;
  rating: number;
}

export interface Order {
  id: string;
  userId: string;
  products: Array<{
    productId: string;
    quantity: number;
    price: number;
  }>;
  total: number;
  status:
    | 'pending'
    | 'confirmed'
    | 'shipped'
    | 'delivered'
    | 'cancelled';
  createdAt: string;
}

export interface TestScenario<T> {
  name: string;
  description: string;
  input: T;
  expected: unknown;
  setup?: () => Promise<void>;
  cleanup?: () => Promise<void>;
}

// data/test-data-factory.ts
class TestDataFactory {
  private static productIdCounter = 1;
  private static orderIdCounter = 1;

  static createProduct(
    overrides: Partial<Product> = {}
  ): Product {
    return {
      id: `product-${this.productIdCounter++}`,
      name: 'Test Product',
      price: 1000,
      category: 'electronics',
      inStock: true,
      rating: 4.5,
      ...overrides,
    };
  }

  static createOrder(
    overrides: Partial<Order> = {}
  ): Order {
    return {
      id: `order-${this.orderIdCounter++}`,
      userId: 'user-1',
      products: [
        {
          productId: 'product-1',
          quantity: 1,
          price: 1000,
        },
      ],
      total: 1000,
      status: 'pending',
      createdAt: new Date().toISOString(),
      ...overrides,
    };
  }

  static createProductScenarios(): TestScenario<Product>[] {
    return [
      {
        name: '通常商品の作成',
        description: '在庫ありの通常商品を作成する',
        input: this.createProduct(),
        expected: { success: true },
      },
      {
        name: '在庫切れ商品の作成',
        description:
          '在庫切れ商品を作成し、適切なメッセージが表示される',
        input: this.createProduct({ inStock: false }),
        expected: {
          success: true,
          warning: '在庫切れ商品です',
        },
      },
      {
        name: '高額商品の作成',
        description: '100,000円以上の高額商品の作成',
        input: this.createProduct({ price: 150000 }),
        expected: { success: true, requiresApproval: true },
      },
    ];
  }
}

// 型安全なテストデータの使用例
test.describe('商品管理機能', () => {
  const scenarios =
    TestDataFactory.createProductScenarios();

  scenarios.forEach((scenario) => {
    test(scenario.name, async ({ page }) => {
      if (scenario.setup) {
        await scenario.setup();
      }

      await page.goto('/admin/products/new');

      // 型安全な商品データの入力
      await page.fill(
        '[data-testid="product-name"]',
        scenario.input.name
      );
      await page.fill(
        '[data-testid="product-price"]',
        scenario.input.price.toString()
      );
      await page.selectOption(
        '[data-testid="product-category"]',
        scenario.input.category
      );

      if (scenario.input.inStock) {
        await page.check(
          '[data-testid="in-stock-checkbox"]'
        );
      }

      await page.click('[data-testid="save-button"]');

      // 期待される結果の検証
      const result = scenario.expected as {
        success: boolean;
        warning?: string;
        requiresApproval?: boolean;
      };

      if (result.success) {
        await expect(
          page.locator('[data-testid="success-message"]')
        ).toBeVisible();
      }

      if (result.warning) {
        await expect(
          page.locator('[data-testid="warning-message"]')
        ).toHaveText(result.warning);
      }

      if (result.requiresApproval) {
        await expect(
          page.locator('[data-testid="approval-required"]')
        ).toBeVisible();
      }

      if (scenario.cleanup) {
        await scenario.cleanup();
      }
    });
  });
});

API テストとの型共有

フロントエンドとバックエンドで API の型定義を共有し、一貫性のあるテストを実現します。

typescript// shared/api-types.ts
export interface ApiResponse<T> {
  data: T;
  status: 'success' | 'error';
  message?: string;
  pagination?: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: string;
  updatedAt: string;
}

export interface CreateUserRequest {
  email: string;
  name: string;
  password: string;
  role?: 'user' | 'guest';
}

export interface UpdateUserRequest {
  name?: string;
  email?: string;
  role?: 'admin' | 'user' | 'guest';
}

// helpers/api-test-helper.ts
class ApiTestHelper {
  constructor(private request: APIRequestContext) {}

  async createUser(
    userData: CreateUserRequest
  ): Promise<ApiResponse<User>> {
    const response = await this.request.post('/api/users', {
      data: userData,
    });

    return (await response.json()) as ApiResponse<User>;
  }

  async getUser(
    userId: string
  ): Promise<ApiResponse<User>> {
    const response = await this.request.get(
      `/api/users/${userId}`
    );
    return (await response.json()) as ApiResponse<User>;
  }

  async updateUser(
    userId: string,
    updateData: UpdateUserRequest
  ): Promise<ApiResponse<User>> {
    const response = await this.request.patch(
      `/api/users/${userId}`,
      {
        data: updateData,
      }
    );

    return (await response.json()) as ApiResponse<User>;
  }

  async deleteUser(
    userId: string
  ): Promise<ApiResponse<null>> {
    const response = await this.request.delete(
      `/api/users/${userId}`
    );
    return (await response.json()) as ApiResponse<null>;
  }

  async getUsers(
    page = 1,
    limit = 10
  ): Promise<ApiResponse<User[]>> {
    const response = await this.request.get(
      `/api/users?page=${page}&limit=${limit}`
    );
    return (await response.json()) as ApiResponse<User[]>;
  }
}

// E2E テストでの API との連携例
test('ユーザー管理の E2E テスト', async ({
  page,
  request,
}) => {
  const apiHelper = new ApiTestHelper(request);

  // API でテストユーザーを作成
  const createResponse = await apiHelper.createUser({
    email: 'test@example.com',
    name: 'Test User',
    password: 'password123',
    role: 'user',
  });

  expect(createResponse.status).toBe('success');
  expect(createResponse.data.email).toBe(
    'test@example.com'
  );

  const userId = createResponse.data.id;

  // UI でユーザー詳細ページを開く
  await page.goto(`/admin/users/${userId}`);

  // API レスポンスと UI の表示が一致することを確認
  await expect(
    page.locator('[data-testid="user-email"]')
  ).toHaveText(createResponse.data.email);
  await expect(
    page.locator('[data-testid="user-name"]')
  ).toHaveText(createResponse.data.name);
  await expect(
    page.locator('[data-testid="user-role"]')
  ).toHaveText(createResponse.data.role);

  // UI でユーザー情報を更新
  await page.click('[data-testid="edit-button"]');
  await page.fill(
    '[data-testid="name-input"]',
    'Updated User'
  );
  await page.click('[data-testid="save-button"]');

  // API で更新されたデータを確認
  const updatedResponse = await apiHelper.getUser(userId);
  expect(updatedResponse.data.name).toBe('Updated User');

  // UI にも反映されていることを確認
  await expect(
    page.locator('[data-testid="user-name"]')
  ).toHaveText('Updated User');

  // クリーンアップ
  await apiHelper.deleteUser(userId);
});

まとめ

TypeScript と Playwright の組み合わせは、E2E テスト自動化において革新的な価値をもたらします。

本記事でご紹介した主要なメリットをまとめると以下の通りです。

メリット具体的な効果開発チームへの影響
型チェックによる品質向上コンパイル時エラー検出テスト実行前のバグ発見
IDE サポートの充実自動補完・リファクタリング支援開発速度の大幅向上
型安全なリファクタリング変更影響範囲の静的検証安全な大規模リファクタリングの実現
自己文書化されたコード型定義による意図の明確化チーム間のコミュニケーション改善
保守性の高いテスト設計Page Object Model の型安全な実装長期的なメンテナンス負荷軽減
API との型整合性フロントエンド・バックエンド連携一貫性のあるデータハンドリング

特に、Page Object Model の TypeScript 実装カスタムフィクスチャの型定義テストデータの型安全な管理は、大規模なプロジェクトにおいて必須のパターンとなります。

JavaScript から TypeScript への移行は、初期の学習コストはありますが、長期的な開発効率と品質の向上において計り知れない価値をもたらします。型システムの恩恵を活用することで、従来のテスト自動化では実現できなかった、保守性が高く、拡張性に優れたテストスイートを構築できるでしょう。

現代の Web 開発において、TypeScript は単なるトレンドではなく、品質の高いソフトウェアを効率的に開発するための必須技術となっています。Playwright と TypeScript の組み合わせは、その未来を実現するための最適な選択肢と言えるでしょう。

関連リンク