自動テストは、テストコードを書き始めてから考えるものではありません。
後からテストを追加しようとしても、設計がテストに向いていないと、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仕様、インターフェース、テスト用属性を設計レビューの確認項目に入れるのがおすすめです。
既存プロジェクトでは、外部依存の切り出しとセレクタの見直しから始めると、比較的少ない変更で効果を出しやすくなります。


