mcpField
F
flutter-testing-skill

flutter-testing-skill

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

flutter-testing-skill について

flutter-testing-skillは、Flutter開発者向けにユニット、ウィジェット、統合、ゴールデンテストを解説します。Dartの例やコマンドに加え、Finder、操作、依存関係のモックに関する指針を提供します。

主な機能

  1. 01ユニット・ウィジェット・統合テストのガイド
  2. 02ウィジェットのFinderと操作パターン
  3. 03ビジュアルリグレッション用ゴールデンテスト
  4. 04Mockitoによる依存関係のモック
  5. 05テスト実行と管理のコマンド

活用シーン

  1. 01Flutter UIの動作と操作のテスト
  2. 02画面とゴールデン画像の比較
  3. 03統合テストの実行やモックの生成

インストール

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

$npx skills add https://github.com/lambdatest/agent-skills --skill flutter-testing-skill

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

同梱ファイルreference/advanced-patterns.mdreference/playbook.md

詳細

Flutter テストスキル

あなたはテストを専門とするシニア Flutter 開発者です。

ステップ 1 — テストの種類

├─ "unit test", "business logic", "model test"
│  └─ Unit test: test/ directory, flutter_test package
│
├─ "widget test", "component test", "UI test"
│  └─ Widget test: test/ directory, testWidgets()
│
├─ "integration test", "E2E", "full app test"
│  └─ Integration test: integration_test/ directory
│
├─ "golden test", "snapshot", "visual regression"
│  └─ Golden test: matchesGoldenFile()
│
└─ Ambiguous? → Widget test (most common)

基本パターン — Dart

ウィジェットテスト(最も一般的)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/screens/login_screen.dart';

void main() {
  testWidgets('Login screen shows email and password fields', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    // Verify fields exist
    expect(find.byType(TextField), findsNWidgets(2));
    expect(find.text('Email'), findsOneWidget);
    expect(find.text('Password'), findsOneWidget);
    expect(find.byType(ElevatedButton), findsOneWidget);
  });

  testWidgets('Login with valid credentials navigates to dashboard', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    // Enter credentials
    await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
    await tester.enterText(find.byKey(const Key('passwordField')), 'password123');

    // Tap login button
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle(); // Wait for animations and navigation

    // Verify navigation
    expect(find.text('Dashboard'), findsOneWidget);
  });

  testWidgets('Shows error for invalid credentials', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginScreen()));

    await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
    await tester.enterText(find.byKey(const Key('passwordField')), 'wrong');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    expect(find.text('Invalid credentials'), findsOneWidget);
  });
}
Finder の戦略
// By Key (best — explicit test identifiers)
find.byKey(const Key('loginButton'))
find.byKey(const ValueKey('email_input'))

// By Type
find.byType(ElevatedButton)
find.byType(TextField)
find.byType(LoginScreen)

// By Text
find.text('Login')
find.textContaining('Welcome')

// By Icon
find.byIcon(Icons.login)

// By Widget predicate
find.byWidgetPredicate((widget) => widget is Text && widget.data!.startsWith('Error'))

// Descendant/Ancestor
find.descendant(of: find.byType(AppBar), matching: find.text('Title'))
find.ancestor(of: find.text('Login'), matching: find.byType(Card))
操作
await tester.tap(finder);                    // Tap
await tester.longPress(finder);              // Long press
await tester.enterText(finder, 'text');      // Type text
await tester.drag(finder, const Offset(0, -300));  // Drag/scroll
await tester.fling(finder, const Offset(0, -500), 1000); // Fling/swipe

// CRITICAL: Always pump after actions
await tester.pump();                         // Single frame
await tester.pump(const Duration(seconds: 1)); // Advance time
await tester.pumpAndSettle();                // Wait for animations to finish
統合テスト
// integration_test/app_test.dart
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();

  testWidgets('Full login flow', (WidgetTester tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Login
    await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
    await tester.enterText(find.byKey(const Key('passwordField')), 'password123');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    // Verify dashboard
    expect(find.text('Dashboard'), findsOneWidget);

    // Navigate to settings
    await tester.tap(find.byIcon(Icons.settings));
    await tester.pumpAndSettle();
    expect(find.text('Settings'), findsOneWidget);
  });
}
ゴールデンテスト(ビジュアルリグレッション)
testWidgets('Login screen matches golden', (WidgetTester tester) async {
  await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
  await tester.pumpAndSettle();

  await expectLater(
    find.byType(LoginScreen),
    matchesGoldenFile('goldens/login_screen.png'),
  );
});
# Generate golden files
flutter test --update-goldens

# Run golden comparison
flutter test
依存関係のモック
// Using Mockito
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([AuthService])
void main() {
  late MockAuthService mockAuth;

  setUp(() {
    mockAuth = MockAuthService();
  });

  testWidgets('Login calls auth service', (tester) async {
    when(mockAuth.login(any, any)).thenAnswer((_) async => true);

    await tester.pumpWidget(MaterialApp(
      home: LoginScreen(authService: mockAuth),
    ));

    await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
    await tester.enterText(find.byKey(const Key('passwordField')), 'pass123');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    verify(mockAuth.login('[email protected]', 'pass123')).called(1);
  });
}
アンチパターン
悪い例良い例理由
操作後に pumpAndSettle() を呼び出さない操作後は必ず pump するアニメーションが完了しない
動的なテキストに find.text() を使うfind.byKey() を使うロケールやテキストの変更でテストが壊れる
実装の詳細をテストするユーザーが使う機能をテストする壊れやすい
ウィジェットテストでモックを使わないサービスやリポジトリをモックするテストが実際の API を呼び出す
TestMu AI Cloud(統合テスト)
# Run integration tests on LambdaTest real devices
# 1. Build app for testing
flutter build apk --debug  # Android
flutter build ios --simulator  # iOS

# 2. Upload to LambdaTest
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@build/app/outputs/flutter-apk/app-debug.apk"

# 3. Run via Appium (Flutter driver)
# Use appium-flutter-driver for element interaction

クイックリファレンス

タスクコマンド
すべてのテストを実行flutter test
特定のファイルを実行flutter test test/login_test.dart
カバレッジ付きで実行flutter test --coverage
統合テストを実行flutter test integration_test/
ゴールデンを更新flutter test --update-goldens
モックを生成flutter pub run build_runner build
特定のプラットフォームでテストflutter test --platform chrome

pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mockito: ^5.4.0
  build_runner: ^2.4.0

詳細なパターン

高度なパターン、デバッグガイド、CI/CD との統合、ベストプラクティスについては、 reference/playbook.md を参照してください。

コメント