mcpField
F
flutter-testing

flutter-testing

github.com/vp-kテスト・品質1437 回インストール11 週間前に更新
リポジトリ

flutter-testing について

flutter-testingはFlutterのテスト作成を支援するスキルです。業務ロジックと状態のテストを優先し、テンプレートや実行方法、検証手順も示します。

主な機能

  1. 01Repositoryとデータソースのテスト
  2. 02BLoCとProviderの状態テスト
  3. 03任意のウィジェットテスト
  4. 04Goldenテストのガイド
  5. 05テストコマンドと依存関係

活用シーン

  1. 01実際のAPIなしでRepositoryをテスト
  2. 02状態遷移とエラー処理の確認
  3. 03デザインコンポーネントの視覚変化を検出

インストール

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

$npx skills add https://github.com/vp-k/flutter-craft --skill flutter-testing

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

詳細

Flutter テストガイド

概要

実装後、優先順位に従ってテストを作成します。まずビジネスロジックに取り組み、UI は最後にします。

開始時に宣言:「テストの作成には flutter-testing スキルを使用します。」

テストの優先順位

Priority 1: Repository & DataSource Unit Tests
  ├── Business logic correctness
  ├── API integration
  └── Data transformation

Priority 2: State Management Unit Tests
  ├── BLoC/Cubit event handling
  ├── Provider state transitions
  └── Error state handling

Priority 3: Widget Tests (Optional)
  ├── User interactions
  ├── Widget rendering
  └── Navigation

Priority 4: Golden Tests (Visual Regression)
  ├── Design system component snapshots
  ├── Pixel-perfect comparison
  └── CI integration

Optional: Integration Tests
  └── Full app flow testing

優先度 1:Repository と DataSource のテスト

Repository テストのテンプレート
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRemoteDataSource extends Mock implements UserRemoteDataSource {}

class MockUserLocalDataSource extends Mock implements UserLocalDataSource {}

void main() {
  late UserRepositoryImpl repository;
  late MockUserRemoteDataSource mockRemoteDataSource;
  late MockUserLocalDataSource mockLocalDataSource;

  setUp(() {
    mockRemoteDataSource = MockUserRemoteDataSource();
    mockLocalDataSource = MockUserLocalDataSource();
    repository = UserRepositoryImpl(
      remoteDataSource: mockRemoteDataSource,
      localDataSource: mockLocalDataSource,
    );
  });

  group('getUser', () {
    const tUserId = '123';
    final tUserModel = UserModel(id: '123', name: 'Test', email: '[email protected]');
    final tUserEntity = User(id: '123', name: 'Test', email: '[email protected]');

    test('should return User when remote data source succeeds', () async {
      // Arrange
      when(() => mockRemoteDataSource.getUser(any()))
          .thenAnswer((_) async => tUserModel);

      // Act
      final result = await repository.getUser(tUserId);

      // Assert
      expect(result, equals(tUserEntity));
      verify(() => mockRemoteDataSource.getUser(tUserId));
    });

    test('should throw Exception when remote data source fails', () async {
      // Arrange
      when(() => mockRemoteDataSource.getUser(any()))
          .thenThrow(Exception('Server error'));

      // Act & Assert
      expect(
        () => repository.getUser(tUserId),
        throwsException,
      );
    });
  });
}
DataSource テストのテンプレート
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements http.Client {}

void main() {
  late UserRemoteDataSourceImpl dataSource;
  late MockHttpClient mockHttpClient;

  setUpAll(() {
    // mocktail needs a fallback for non-primitive matcher types
    registerFallbackValue(Uri.parse('https://example.com'));
  });

  setUp(() {
    mockHttpClient = MockHttpClient();
    dataSource = UserRemoteDataSourceImpl(client: mockHttpClient);
  });

  group('getUser', () {
    const tUserId = '123';
    final tUserJson = '{"id": "123", "name": "Test", "email": "[email protected]"}';

    test('should return UserModel when response is 200', () async {
      // Arrange
      when(() => mockHttpClient.get(any(), headers: any(named: 'headers')))
          .thenAnswer((_) async => http.Response(tUserJson, 200));

      // Act
      final result = await dataSource.getUser(tUserId);

      // Assert
      expect(result, isA<UserModel>());
      expect(result.id, equals('123'));
    });

    test('should throw ServerException when response is not 200', () async {
      // Arrange
      when(() => mockHttpClient.get(any(), headers: any(named: 'headers')))
          .thenAnswer((_) async => http.Response('Error', 500));

      // Act & Assert
      expect(
        () => dataSource.getUser(tUserId),
        throwsA(isA<ServerException>()),
      );
    });
  });
}

優先度 2:状態管理のテスト

BLoC テストのテンプレート
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockGetUserUseCase extends Mock implements GetUserUseCase {}

void main() {
  late UserBloc bloc;
  late MockGetUserUseCase mockGetUser;

  setUp(() {
    mockGetUser = MockGetUserUseCase();
    bloc = UserBloc(getUser: mockGetUser);
  });

  tearDown(() {
    bloc.close();
  });

  test('initial state should be UserInitial', () {
    expect(bloc.state, equals(UserInitial()));
  });

  blocTest<UserBloc, UserState>(
    'should emit [Loading, Loaded] when GetUser succeeds',
    build: () {
      when(() => mockGetUser(any()))
          .thenAnswer((_) async => const User(id: '1', name: 'Test'));
      return bloc;
    },
    act: (bloc) => bloc.add(const GetUserEvent('1')),
    expect: () => [
      UserLoading(),
      const UserLoaded(User(id: '1', name: 'Test')),
    ],
  );

  blocTest<UserBloc, UserState>(
    'should emit [Loading, Error] when GetUser fails',
    build: () {
      when(() => mockGetUser(any())).thenThrow(Exception('error'));
      return bloc;
    },
    act: (bloc) => bloc.add(const GetUserEvent('1')),
    expect: () => [
      UserLoading(),
      const UserError('error'),
    ],
  );
}
Provider/Riverpod テストのテンプレート
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late ProviderContainer container;
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
    container = ProviderContainer(
      overrides: [
        userRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  test('should return user when fetchUser succeeds', () async {
    // Arrange
    when(() => mockRepository.getUser(any()))
        .thenAnswer((_) async => const User(id: '1', name: 'Test'));

    // Act
    final result = await container.read(userProvider('1').future);

    // Assert
    expect(result.name, equals('Test'));
  });
}

優先度 3:ウィジェットテスト(任意)

ウィジェットテストのテンプレート
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mocktail/mocktail.dart';

class MockUserBloc extends MockBloc<UserEvent, UserState> implements UserBloc {}

void main() {
  late MockUserBloc mockBloc;

  setUp(() {
    mockBloc = MockUserBloc();
  });

  Widget createWidgetUnderTest() {
    return MaterialApp(
      home: BlocProvider<UserBloc>.value(
        value: mockBloc,
        child: const UserScreen(),
      ),
    );
  }

  testWidgets('should display loading indicator when state is Loading',
      (tester) async {
    // Arrange
    when(() => mockBloc.state).thenReturn(UserLoading());

    // Act
    await tester.pumpWidget(createWidgetUnderTest());

    // Assert
    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('should display user name when state is Loaded',
      (tester) async {
    // Arrange
    when(() => mockBloc.state).thenReturn(
      const UserLoaded(User(id: '1', name: 'John Doe')),
    );

    // Act
    await tester.pumpWidget(createWidgetUnderTest());

    // Assert
    expect(find.text('John Doe'), findsOneWidget);
  });

  testWidgets('should call GetUserEvent when button is tapped',
      (tester) async {
    // Arrange
    when(mockBloc.state).thenReturn(UserInitial());

    // Act
    await tester.pumpWidget(createWidgetUnderTest());
    await tester.tap(find.byType(ElevatedButton));

    // Assert
    verify(mockBloc.add(any)).called(1);
  });
}

優先度 4:ゴールデンテスト(ビジュアルリグレッション)

デザインシステムのコンポーネントのビジュアルの一貫性を検証します。意図しない UI の変更を防ぎます。

ゴールデンテストのテンプレート
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('LoginButton matches golden', (tester) async {
    // Fixed size guarantees consistent screenshots
    await tester.binding.setSurfaceSize(const Size(400, 200));

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: LoginButton(onPressed: () {}),
          ),
        ),
      ),
    );

    await expectLater(
      find.byType(LoginButton),
      matchesGoldenFile('goldens/login_button.png'),
    );
  });

  testWidgets('UserCard dark mode matches golden', (tester) async {
    await tester.binding.setSurfaceSize(const Size(400, 300));

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.dark(),
        home: Scaffold(
          body: UserCard(
            user: User(id: '1', name: 'Test User', avatar: 'assets/test/avatar.png'),
          ),
        ),
      ),
    );

    await expectLater(
      find.byType(UserCard),
      matchesGoldenFile('goldens/user_card_dark.png'),
    );
  });
}
ゴールデンファイルの管理
# Initial generation / baseline update (after intentional changes)
flutter test --update-goldens

# Comparison run (used in CI/CD)
flutter test --tags golden

# Specific files only
flutter test test/features/auth/presentation/widgets/login_button_golden_test.dart --update-goldens
テストファイルの命名規則

ゴールデンテストファイルは、_golden_test.dart というサフィックスで区別します:

test/features/auth/presentation/widgets/
├── login_button_test.dart         # Functional test (Priority 3)
└── login_button_golden_test.dart  # Golden test (Priority 4)
タグベースの実行

dart_test.yaml でタグを付けてゴールデンテストを分けます:

tags:
  golden:
    # Can be run separately in CI

テストファイルにタグを追加します:

@Tags(['golden'])
library;

import 'package:flutter_test/flutter_test.dart';
// ...
ゴールデンテストを作成する際の注意点
  1. 固定サイズ:setSurfaceSize() で一貫したキャンバスサイズを設定します
  2. 決定性のあるデータ:ランダムなデータや時間に依存するデータは使用しないでください
  3. フォントの読み込み:カスタムフォントは FontLoader で事前に読み込みます
  4. ネットワーク画像:モックに置き換えます(ネットワークへの依存をなくします)
  5. プラットフォームの違い:macOS/Linux/Windows 間でレンダリングが異なる場合があります → CI 環境を固定します
ゴールデンテストを作成する場合
  • デザインシステムのコンポーネント(Button、Card、Input、Badge など)
  • テーマ変更後のビジュアルへの影響の検証
  • ダークモード/ライトモードの切り替えの検証
  • 各ブレークポイントでのレスポンシブレイアウトの検証
ゴールデンテストを省略する場合
  • ビジネスロジックが中心の画面(表示データだけが異なる場合)
  • 頻繁に変更される画面(ゴールデンファイルの更新コストがメリットを上回る場合)
  • 外部データへの依存度が高い画面

テストファイルの構成

test/
├── features/
│   └── auth/
│       ├── data/
│       │   ├── datasources/
│       │   │   └── auth_remote_datasource_test.dart
│       │   └── repositories/
│       │       └── auth_repository_impl_test.dart
│       └── presentation/
│           ├── bloc/
│           │   └── auth_bloc_test.dart
│           └── widgets/
│               └── login_button_test.dart
└── helpers/
    ├── test_helpers.dart
    └── pump_app.dart

テストの実行

# All tests
flutter test

# Specific feature
flutter test test/features/auth/

# Specific file
flutter test test/features/auth/data/repositories/auth_repository_impl_test.dart

# With coverage
flutter test --coverage

# Generate coverage report
genhtml coverage/lcov.info -o coverage/html

テストの依存関係

# Mocking — mocktail (no codegen; all templates in this skill use it)
flutter pub add dev:mocktail

# State management testing
flutter pub add dev:bloc_test      # If using BLoC (also provides MockBloc)

# Freezed (if using immutable states)
flutter pub add freezed_annotation
flutter pub add dev:freezed
flutter pub add dev:build_runner   # For freezed codegen only

コード生成(Freezed の状態など)

dart run build_runner build --delete-conflicting-outputs

主な原則

  1. 優先順位: Repository → 状態 → ウィジェット
  2. 依存関係のモック化: 実際の API やデータベースはテストしません
  3. Arrange-Act-Assert: テストの構成を明確にします
  4. アサーションの焦点を 1 つにする: 各テストでは 1 つのことをテストします
  5. 説明的な名前: テスト名で動作を説明します

テストを省略する場合

  • ウィジェットテストを省略する場合:

    • 単純なステートレスウィジェット
    • ユーザー操作のロジックがない
    • 表示のみ(ビジネスロジックがない)
  • 決して省略しないもの:

    • Repository のテスト(ビジネスロジック)
    • 状態管理のテスト(状態遷移)
    • エラーハンドリングのテスト

必須のサブスキル

テストの作成後、必ず次を実行してください: → flutter-craft:flutter-verification

flutter test を実行し、すべてのテストが成功することを確認します。

コメント