Flutterでユニットテスト・Widgetテスト・インテグレーションテストを実践した話
Flutterアプリの品質を高めるため、テスト体系を整備しました。
今回は、Unit Test、Widget Test、Integration Testの3種類のテストを実例と共に紹介します。
テストの種類と特徴
| 種類 | 目的 | 特徴 | 実行速度 |
|---|---|---|---|
| Unit Test | ビジネスロジック検証 | メソッドや関数単位のテスト | 非常に高速 |
| Widget Test | UI単体動作確認 | ウィジェット単位の動作確認、UIのレンダリングも検証可能 | 高速 |
| Integration Test | アプリ全体の動作確認 | 実機に近い形で画面遷移やデータ連携をテスト | 遅め(時間がかかる) |
ポイント: まずUnit Testでロジックを安定させ、Widget TestでUIの振る舞いを検証、
最後にIntegration Testで全体のフローを確認するのが効率的。
Unit Test
int multiply(int a, int b) => a * b;
void main() {
test('multiply test', () {
expect(multiply(3, 4), 12);
});
test('multiply by zero', () {
expect(multiply(5, 0), 0);
});
}
メリット:
- バグの早期発見
- リファクタリング時の安全性確保
- テストの実行が高速
Widget Test
testWidgets('Button tap changes text', (tester) async {
await tester.pumpWidget(MyApp());
// 初期状態の確認
expect(find.text('Tapped'), findsNothing);
// ボタンをタップ
await tester.tap(find.text('Tap'));
await tester.pump();
// 結果の確認
expect(find.text('Tapped'), findsOneWidget);
});
メリット:
- UIの動作を自動化して検証可能
- レイアウト崩れや状態管理の問題を早期発見
- CI/CDで自動実行可能
Integration Test
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Login flow', (tester) async {
await tester.pumpWidget(MyApp());
// ユーザー名入力
await tester.enterText(find.byType(TextField), 'user');
// ログインボタンをタップ
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// ホーム画面に遷移しているか確認
expect(find.text('Home'), findsOneWidget);
});
}
メリット:
- 実際のユーザー操作を模擬できる
- 画面遷移やネットワーク通信の挙動も確認可能
- バグ発見の確実性が高い
テスト導入で得られた効果
- リファクタ安全性向上: テストがあるため、自信を持ってコードを書き換えられる
- 仕様理解が深まる: コードを書く前に「どういう動作を期待するか」を整理できる
- バグ混入率低下: 開発中に不具合を早期発見できる
- CI/CDとの相性が良い: GitHub ActionsやCodemagicで自動テスト可能
Tips & Best Practices
- 小さな単位からテストを作る: Unit Test → Widget Test → Integration Testの順で段階的に導入
- 外部依存はモック化: HTTP通信やDB操作はmockitoやmocktailでモック化
- テストコードもドキュメント化: テストの意図や前提条件をコメントに書く
- CIでの自動化: すぐにテストを実行できる環境を整える
まとめ
Flutterはテスト環境が非常に整っており、小規模プロジェクトでも導入効果が高いです。
テストを整備することで、リファクタリングが安全になり、開発速度も向上します。
「テストを書く時間は、後でバグに悩まされる時間の短縮につながる」と感じました。