mcpField
F
flutter-add-integration-test

flutter-add-integration-test

github.com/dhruvanbhalaraテスト・品質3050 回インストール9 週間前に更新
リポジトリ

flutter-add-integration-test について

flutter-add-integration-testは、Flutterプロジェクトに統合テストを追加して実行する手順を案内するスキルです。テストの設定と作成、デバイス・Webでの実行、パフォーマンス記録、CIの構成を扱います。

主な機能

  1. 01統合テスト依存関係の設定
  2. 02ウィジェット操作とアサーションの作成
  3. 03デバイス・Webでのテスト実行
  4. 04パフォーマンスタイムラインの取得
  5. 05CI統合テストの設定

活用シーン

  1. 01画面をまたぐアプリフローのテスト
  2. 02キー付きウィジェットの操作確認
  3. 03Chromeでの統合テスト実行
  4. 04テスト操作の性能分析

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dhruvanbhalara/skills --skill flutter-add-integration-test

インストール後、Claude Code で/flutter-add-integration-testを入力して読み込まれたか確認します。

詳細

目次

プロジェクトのセットアップ

  1. 必要な開発依存関係を pubspec.yaml に追加します:

    flutter pub add 'dev:integration_test:{"sdk":"flutter"}'
    flutter pub add 'dev:flutter_test:{"sdk":"flutter"}'
    
  2. ディレクトリ構造を作成します:

    project_root/
    ├── integration_test/
    │   └── app_test.dart          # テストケース
    └── test_driver/
        └── integration_test.dart  # ホストドライバースクリプト
    
  3. ホストドライバースクリプトを test_driver/integration_test.dart に作成します:

    import 'package:integration_test/integration_test_driver.dart';
    
    Future<void> main() => integrationDriver();
    
  4. 確実にターゲット指定できるよう、本番コード内の重要なウィジェットに ValueKey を追加します:

    FloatingActionButton(
      key: const ValueKey('increment_fab'),
      onPressed: _increment,
      child: const Icon(Icons.add),
    )
    

テストの作成

  • main() の先頭でバインディングを初期化します。これはデフォルトのテストバインディングを置き換えます。
  • tester.pumpWidget(const MyApp()) でアプリケーション全体をロードします。
  • アニメーションや非同期処理を待つため、すべての操作の後に tester.pumpAndSettle() を使用します。
  • expect(find.byKey(ValueKey('foo')), findsOneWidget) を使用してウィジェットの表示を検証します。
  • 画面外のウィジェットまでスクロールするには tester.scrollUntilVisible(finder, 500.0) を使用します。
テストファイルの構造
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('End-to-end test', () {
    testWidgets('complete user flow', (tester) async {
      // Load full app
      await tester.pumpWidget(const MyApp());

      // Interact with widgets
      await tester.tap(find.byKey(const ValueKey('login_button')));
      await tester.pumpAndSettle();

      // Assert navigation happened
      expect(find.byType(HomePage), findsOneWidget);
    });
  });
}

実行ターゲット

対象プラットフォームに基づいて実行方法を選択します:

ローカルデバイス (Android/iOS)
flutter test integration_test/

iOS の場合、デフォルトの Swift Package Manager (SPM) 連携 (Flutter 3.44+) で実行する際は、先にパッケージ解決を実行してください:

xcodebuild -resolvePackageDependencies -workspace ios/Runner.xcworkspace -scheme Runner
Chrome (Web)
# Terminal 1: Start ChromeDriver
chromedriver --port=4444

# Terminal 2: Run tests
flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart \
  -d chrome
ヘッドレス Web
flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart \
  -d web-server
Firebase Test Lab (Android)
# 1. Build debug APK
flutter build apk --debug

# 2. Build instrumentation test APK
pushd android && ./gradlew app:assembleAndroidTest && popd

# 3. Upload both APKs to Firebase Test Lab via console or gcloud:
gcloud firebase test android run \
  --type instrumentation \
  --app build/app/outputs/flutter-apk/app-debug.apk \
  --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk

パフォーマンスプロファイリング

パフォーマンスのタイムラインを取得するには、テストアクションを binding.traceAction() でラップします:

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('scrolling performance', (tester) async {
    await tester.pumpWidget(const MyApp());

    await binding.traceAction(() async {
      final listFinder = find.byType(Scrollable);
      await tester.fling(listFinder, const Offset(0, -500), 10000);
      await tester.pumpAndSettle();
    }, reportKey: 'scrolling_timeline');
  });
}
パフォーマンスプロファイリングドライバー

このドライバーを使用してタイムラインデータを取得し書き出します:

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
          data['scrolling_timeline'] as Map<String, dynamic>,
        );
        final summary = driver.TimelineSummary.summarize(timeline);
        await summary.writeTimelineToFile(
          'scrolling_timeline',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

CI/CD 連携

GitHub Actions ワークフロー
- name: Run integration tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 33
    script: flutter test integration_test/ --flavor dev
  • Android エミュレーターには reactivecircus/android-emulator-runner を使用します。
  • actions/upload-artifact でテスト結果のアーティファクトを収集します。
  • Web の場合は、chromedriver をサービスとして実行し、-d chrome でテストします。

よくある落とし穴

エラー原因修正
PumpAndSettleTimedOutException無限アニメーション (例: CircularProgressIndicator)代わりに pump() を使用するか、読み込み状態を解除します
ウィジェットが見つからないSliverList や ListView での遅延読み込み操作する前に scrollUntilVisible() を呼び出します
テストがハングする本番コード内のネットワーク呼び出しHTTP クライアントをモックするか、--dart-define を使用して回避します
No host driver specifiedtest_driver/integration_test.dart が存在しないホストドライバーファイルを作成します

ワークフロー: 統合テストの追加

タスクの進捗
  • ステップ 1: integration_test と flutter_test を dev_dependencies に追加します。
  • ステップ 2: 本番コード内の対象ウィジェットに ValueKey を割り当てます。
  • ステップ 3: バインディングの初期化を含む integration_test/app_test.dart を作成します。
  • ステップ 4: integrationDriver() を含む test_driver/integration_test.dart を作成します。
  • ステップ 5: テストケースを作成します: アプリのロード、操作、検証。
  • ステップ 6: 実行ターゲットを選択します:
    • ローカルデバイス: flutter test integration_test/
    • Chrome: flutter drive ... -d chrome
    • Firebase Test Lab: APK をビルドしてアップロード
  • ステップ 7: フィードバックループ:
    • PumpAndSettleTimedOutException が発生した場合 → 無限アニメーションを確認します。
    • ウィジェットが見つからない場合 → scrollUntilVisible を追加します。
    • すべてのテストがパスするまで再実行します。

例

標準的な統合テスト
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Counter app', () {
    testWidgets('tap FAB, verify counter increments', (tester) async {
      await tester.pumpWidget(const MyApp());

      // Verify initial state
      expect(find.text('0'), findsOneWidget);

      // Tap the increment button
      final fab = find.byKey(const ValueKey('increment_fab'));
      await tester.tap(fab);
      await tester.pumpAndSettle();

      // Verify counter incremented
      expect(find.text('1'), findsOneWidget);
    });
  });
}
Multi-Screen Navigation Flow
testWidgets('login and navigate to home', (tester) async {
  await tester.pumpWidget(const MyApp());

  // Enter credentials
  await tester.enterText(find.byKey(const ValueKey('email_field')), '[email protected]');
  await tester.enterText(find.byKey(const ValueKey('password_field')), 'password123');

  // Submit login
  await tester.tap(find.byKey(const ValueKey('login_button')));
  await tester.pumpAndSettle();

  // Verify navigation to home
  expect(find.byType(HomePage), findsOneWidget);
  expect(find.byType(LoginPage), findsNothing);
});

コメント