テスト
Flutterでのテストは、アプリケーションの品質を保証するために重要です。以下に、テストの方法を紹介します。
ユニットテスト
Section titled “ユニットテスト”ユニットテストは、個々の関数やメソッドが正しく動作することを確認するためのテストです。testパッケージを使用して、ユニットテストを実行します。
ユニットテストの例
Section titled “ユニットテストの例”test('Counter increments smoke test', () { final counter = Counter();
counter.increment();
expect(counter.value, 1);});test: テストケースを定義するための関数です。expect: 実際の値と期待される値を比較します。
ウィジェットテスト
Section titled “ウィジェットテスト”ウィジェットテストは、UIの動作を確認するためのテストです。flutter_testパッケージを使用して、ウィジェットテストを実行します。
ウィジェットテストの例
Section titled “ウィジェットテストの例”testWidgets('MyWidget has a title and message', (WidgetTester tester) async { await tester.pumpWidget(MyWidget());
final titleFinder = find.text('T'); final messageFinder = find.text('M');
expect(titleFinder, findsOneWidget); expect(messageFinder, findsOneWidget);});testWidgets: ウィジェットテストを定義するための関数です。pumpWidget: ウィジェットをテスト環境にロードします。
その他のテスト手法
Section titled “その他のテスト手法”インテグレーションテスト: アプリ全体の動作を確認するためのテストです。integration_testパッケージを使用して、インテグレーションテストを実行します。モックテスト: 外部依存をモック化して、特定の機能をテストします。
実践的なアドバイス
Section titled “実践的なアドバイス”テストを行う際のベストプラクティスや、よくある問題の解決策を以下に示します。
ベストプラクティス
Section titled “ベストプラクティス”- テストの自動化: CI/CDパイプラインにテストを組み込み、自動化することで、継続的な品質保証を実現します。
- テストのカバレッジ: テストカバレッジを高めるために、重要な機能やエッジケースを網羅するテストケースを作成します。
- テストの分離: 各テストケースは独立して実行できるように設計し、他のテストに依存しないようにします。
よくある問題の解決策
Section titled “よくある問題の解決策”-
テストの失敗: テストが失敗した場合、エラーメッセージをよく読み、原因を特定して修正します。
flutter test --verboseを使用して、詳細なログを確認します。 -
非同期処理のテスト: 非同期処理をテストする際は、
async/awaitを使用して、テストが完了するまで待機します。
test('Async test example', () async { final result = await fetchData(); expect(result, isNotNull);});- モックの使用: 外部依存をモック化して、特定の機能をテストします。
mockitoパッケージを使用して、モックを作成します。
3. モックの使い方(mockito)
Section titled “3. モックの使い方(mockito)”pubspec.yamlへの追加
Section titled “pubspec.yamlへの追加”dev_dependencies: mockito: ^5.4.0 build_runner: ^2.4.0モッククラスの作成
Section titled “モッククラスの作成”abstract class UserRepository { Future<User> getUserById(String id);}
// user_repository_test.mocks.dart (自動生成)import 'package:mockito/mockito.dart';import 'package:mockito/annotations.dart';import 'user_repository.dart';
@GenerateMocks([UserRepository])void main() {}モックを使用したテスト
Section titled “モックを使用したテスト”import 'package:flutter_test/flutter_test.dart';import 'package:mockito/mockito.dart';import 'package:mockito/annotations.dart';import 'user_repository_test.mocks.dart';
@GenerateMocks([UserRepository])void main() { late MockUserRepository mockRepository; late UserService userService;
setUp(() { mockRepository = MockUserRepository(); userService = UserService(mockRepository); });
test('getUserById returns user when repository succeeds', () async { // Arrange final expectedUser = User(id: '1', name: 'Test User'); when(mockRepository.getUserById('1')) .thenAnswer((_) async => expectedUser);
// Act final result = await userService.getUserById('1');
// Assert expect(result, equals(expectedUser)); verify(mockRepository.getUserById('1')).called(1); });
test('getUserById throws exception when repository fails', () async { // Arrange when(mockRepository.getUserById('1')) .thenThrow(Exception('User not found'));
// Act & Assert expect( () => userService.getUserById('1'), throwsException, ); });}モックの生成コマンド
Section titled “モックの生成コマンド”flutter pub run build_runner build4. インテグレーションテストの実装
Section titled “4. インテグレーションテストの実装”integration_testディレクトリの作成
Section titled “integration_testディレクトリの作成”プロジェクトルートにintegration_testディレクトリを作成します。
インテグレーションテストの例
Section titled “インテグレーションテストの例”import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:my_app/main.dart' as app;
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () { testWidgets('complete user flow', (WidgetTester tester) async { app.main(); await tester.pumpAndSettle();
// ログイン画面の確認 expect(find.text('ログイン'), findsOneWidget);
// ログイン await tester.enterText(find.byKey(Key('email')), 'test@example.com'); await tester.enterText(find.byKey(Key('password')), 'password'); await tester.tap(find.byKey(Key('loginButton'))); await tester.pumpAndSettle();
// ホーム画面の確認 expect(find.text('ホーム'), findsOneWidget);
// アイテムの追加 await tester.tap(find.byKey(Key('addButton'))); await tester.pumpAndSettle(); expect(find.text('新しいアイテム'), findsOneWidget); }); });}インテグレーションテストの実行
Section titled “インテグレーションテストの実行”# デバイスで実行flutter test integration_test/app_test.dart
# 特定のデバイスで実行flutter test integration_test/app_test.dart -d <device_id>5. 実務でのテスト戦略
Section titled “5. 実務でのテスト戦略”テストピラミッド
Section titled “テストピラミッド” /\ / \ E2E Tests (少数) /____\ / \ Integration Tests (中程度) /________\ / \ Unit Tests (多数) /____________\テストの分類
Section titled “テストの分類”- ユニットテスト: 個々の関数やクラスをテスト
- ウィジェットテスト: UIコンポーネントをテスト
- インテグレーションテスト: 複数のコンポーネントの連携をテスト
- E2Eテスト: アプリ全体の動作をテスト
テストカバレッジの確認
Section titled “テストカバレッジの確認”# カバレッジレポートの生成flutter test --coverage
# カバレッジの確認(lcovが必要)genhtml coverage/lcov.info -o coverage/html6. CI/CDへの組み込み方法
Section titled “6. CI/CDへの組み込み方法”GitHub Actionsの例
Section titled “GitHub Actionsの例”name: Flutter Tests
on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.16.0'
- name: Install dependencies run: flutter pub get
- name: Run tests run: flutter test
- name: Generate coverage run: flutter test --coverage
- name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.infoGitLab CIの例
Section titled “GitLab CIの例”stages: - test
flutter_test: stage: test image: cirrusci/flutter:latest script: - flutter pub get - flutter test - flutter test --coverage coverage: '/lines\.*: \d+\.\d+%/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/lcov.info7. テストのベストプラクティス
Section titled “7. テストのベストプラクティス”AAA パターン(Arrange-Act-Assert)
Section titled “AAA パターン(Arrange-Act-Assert)”test('example test', () { // Arrange: テストの準備 final calculator = Calculator();
// Act: テスト対象の実行 final result = calculator.add(2, 3);
// Assert: 結果の検証 expect(result, equals(5));});テストの独立性
Section titled “テストの独立性”各テストは独立して実行できるようにします。
class TestHelper { static UserRepository createMockRepository() { return MockUserRepository(); }
static User createTestUser() { return User(id: '1', name: 'Test User'); }}
void main() { test('test 1', () { final repository = TestHelper.createMockRepository(); // テスト実行 });
test('test 2', () { final repository = TestHelper.createMockRepository(); // テスト実行(test 1に依存しない) });}テストデータの管理
Section titled “テストデータの管理”class TestData { static User validUser() => User( id: '1', name: 'Test User', email: 'test@example.com', );
static User invalidUser() => User( id: '', name: '', email: '', );}8. よくある問題と解決策
Section titled “8. よくある問題と解決策”問題1: 非同期処理のテストが失敗する
Section titled “問題1: 非同期処理のテストが失敗する”解決策: pumpAndSettleやpumpを使用して、非同期処理の完了を待ちます。
testWidgets('async test', (WidgetTester tester) async { await tester.pumpWidget(MyWidget()); await tester.pumpAndSettle(); // すべてのアニメーションが完了するまで待つ expect(find.text('Loaded'), findsOneWidget);});問題2: モックが正しく動作しない
Section titled “問題2: モックが正しく動作しない”解決策: build_runnerでモックを再生成します。
flutter pub run build_runner build --delete-conflicting-outputs問題3: テストが遅い
Section titled “問題3: テストが遅い”解決策:
- 不要な
pumpAndSettleを避ける - モックを使用して外部依存を排除
- テストを並列実行
flutter test --concurrency=4これで、Flutterでのテスト実装方法を理解できるようになりました。