自動テストは、テストコードを書き始めてから考えるものではありません。
後からテストを追加しようとしても、設計がテストに向いていないと、Mockの準備や画面操作の安定化に多くの工数がかかります。
本記事では、Webアプリケーション開発を前提に、自動テストしやすいプロジェクト設計のポイントを、アーキテクチャ、インターフェース設計、E2Eテスト、ユニットテストの観点から整理します。

目次

1. 責務を分けてテストしやすい構造にする
2. API仕様を先に決めて並行開発を進める
3. インターフェースとDIで外部依存を切り替える
4. E2Eテストを壊れにくくするセレクタを設計する
5. Mockを活用してユニットテストを安定させる
6. まとめ

1. 責務を分けてテストしやすい構造にする

自動テストを成立させるには、設計段階から「どこを、どの単位で検証するか」を決めておくことが重要です。
特にWebアプリケーションでは、フロントエンド、バックエンド、外部サービスの責務を分けることで、テスト対象を明確にできます。

観点

設計のポイント

アーキテクチャ

バックエンドとフロントエンドの責務を分離する

インターフェース設計

外部接続部分を抽象化し、差し替え可能にする

テストコード設計

安定性と保守性を両立するセレクタを用意する

表1:自動テストしやすい設計の主な観点

 

責務が曖昧なままだと、テストごとに本番相当の環境が必要になり、実行時間や失敗原因の切り分けが難しくなります。

2. API仕様を先に決めて並行開発を進める

フロントエンドとバックエンドを分けて開発する場合は、両者の境界となるAPI仕様を先に合意することが効果的です。
REST APIを利用する場合は、OpenAPIやSwagger形式で仕様を明文化しておくと、認識違いを減らせます。

API仕様を先に決めると、フロントエンドはMockサーバを使って先行開発できます。
バックエンド側も、仕様に沿って実装とテストを進められるため、結合時の手戻りを減らせます。

図1:フロントエンドとバックエンドの境界契約

3. インターフェースとDIで外部依存を切り替える

外部API、データベース、メール送信などを直接呼び出す設計では、テスト環境の準備が重くなります。
そこで、外部接続処理をインターフェースとして抽象化し、DI(依存性の注入)で本番用とテスト用の実装を切り替えます。

以下、C#で実装する場合の例を記載します。

  • インターフェース定義

// 外部メール送信の抽象化

public interface IEmailSender

{

Task SendAsync(string to, string subject, string body);

}

  • 本番用実装

public class SmtpEmailSender : IEmailSender

{

public async Task SendAsync(string to, string subject, string body)

{

// 実際のSMTP送信処理

}

}

  • テスト用Mock実装

public class MockEmailSender : IEmailSender

{

public bool WasCalled { get; private set; } = false;

public Task SendAsync(string to, string subject, string body)

{

WasCalled = true;

return Task.CompletedTask;

}

}

  • DIコンテナへの登録

// Program.cs

if (builder.Environment.IsDevelopment())

{

  builder.Services.AddScoped<IEmailSender, MockEmailSender>();

}

else

{

  builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

}

この構成にすると、ビジネスロジックのコードを変更せずに、起動設定だけで本番用とテスト用を切り替えられます。
ASP.NETではappsettings.json、appsettings.Development.json、appsettings.Test.jsonのように、環境ごとに設定を分ける方法があります。

4. E2Eテストを壊れにくくするセレクタを設計する

E2Eテストでは、ユーザー操作をブラウザ上で再現し、画面表示からバックエンドAPIまでを通しで検証します。
代表的なツールにはPlaywright、Selenium、Cypressなどがあります。
ただし、テキストやDOM構造に依存したセレクタは、画面変更のたびに壊れやすくなります。

  • 壊れやすいセレクタの例

// テキストやDOM構造に依存しているため、UI変更で壊れやすい

click('text=決定');

click('.form-container > div:nth-child(2) > button');

  • テスト用属性を付与した例

<!-- 設計段階でdata-testid属性を付与しておく※チーム方針によってはaria属性やroleベースのセレクタを選ぶ場合もあります -->

<form data-testid="shipping-form">

  <button type="submit">決定</button>

</form>

<form data-testid="payment-form">

  <button type="submit">決定</button>

</form>

// data-testidで一意に特定する

click('[data-testid="shipping-form"] button[type="submit"]');

click('[data-testid="payment-form"] button[type="submit"]');

すべての子要素に個別のテストIDを付ける必要はありません。
一意に特定できる親要素を起点にし、その配下はCSSセレクタや相対的なロケータでたどる設計が現実的です。

5. Mockを活用してユニットテストを安定させる

ユニットテストでは、外部依存をMockに置き換えることで、テスト対象をビジネスロジックに絞れます。
インターフェースが定義されていれば、手書きのMockだけでなく、自動生成ツールも活用できます。

ツール

言語・用途

概要

Moq

C# / .NET

インターフェースからMockを動的生成する

Mockito

Java

Javaで広く使われるMockフレームワーク

unittest.mock

Python

Python標準ライブラリのMock機能

Mockoon

言語不問

GUIでAPIのMockサーバを構築できる

表2:Mock作成に利用できる代表的なツール

 

  • ユニットテストの例

public class OrderServiceTests

{

[Fact]

public async Task CreateOrder_ShouldSendConfirmationEmail()

{

// Arrange

var mockEmailSender = new MockEmailSender();

var service = new OrderService(mockEmailSender);

// Act

await service.CreateOrderAsync(new OrderRequest { ... });

// Assert

Assert.True(mockEmailSender.WasCalled);

}

}

6. まとめ

自動テストを成功させるには、テストコードの書き方だけでなく、テストしやすい設計を最初から組み込むことが重要です。

フェーズ

チェック項目

設計フェーズ

バックエンドとフロントエンドの境界をAPI仕様で定義した

設計フェーズ

外部接続処理をインターフェースで抽象化した

設計フェーズ

DIで本番用とテスト用の実装を切り替えられるようにした

設計フェーズ

HTML要素にdata-testidなどのテスト用属性を設計した

実装フェーズ

環境ごとの設定ファイルでテスト用設定を分離した

実装フェーズ

MockツールやMockサーバでテストの独立性を確保した

表3:自動テストしやすい設計のチェックリスト

 

新規プロジェクトでは、API仕様、インターフェース、テスト用属性を設計レビューの確認項目に入れるのがおすすめです。
既存プロジェクトでは、外部依存の切り出しとセレクタの見直しから始めると、比較的少ない変更で効果を出しやすくなります。