flutter-testing について
flutter-testingはFlutterのテスト作成を支援するスキルです。業務ロジックと状態のテストを優先し、テンプレートや実行方法、検証手順も示します。
主な機能
- 01Repositoryとデータソースのテスト
- 02BLoCとProviderの状態テスト
- 03任意のウィジェットテスト
- 04Goldenテストのガイド
- 05テストコマンドと依存関係
活用シーン
- 01実際のAPIなしでRepositoryをテスト
- 02状態遷移とエラー処理の確認
- 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';
// ...
ゴールデンテストを作成する際の注意点
- 固定サイズ:
setSurfaceSize()で一貫したキャンバスサイズを設定します - 決定性のあるデータ:ランダムなデータや時間に依存するデータは使用しないでください
- フォントの読み込み:カスタムフォントは
FontLoaderで事前に読み込みます - ネットワーク画像:モックに置き換えます(ネットワークへの依存をなくします)
- プラットフォームの違い: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
主な原則
- 優先順位: Repository → 状態 → ウィジェット
- 依存関係のモック化: 実際の API やデータベースはテストしません
- Arrange-Act-Assert: テストの構成を明確にします
- アサーションの焦点を 1 つにする: 各テストでは 1 つのことをテストします
- 説明的な名前: テスト名で動作を説明します
テストを省略する場合
-
ウィジェットテストを省略する場合:
- 単純なステートレスウィジェット
- ユーザー操作のロジックがない
- 表示のみ(ビジネスロジックがない)
-
決して省略しないもの:
- Repository のテスト(ビジネスロジック)
- 状態管理のテスト(状態遷移)
- エラーハンドリングのテスト
必須のサブスキル
テストの作成後、必ず次を実行してください: → flutter-craft:flutter-verification
flutter test を実行し、すべてのテストが成功することを確認します。
コメント
Markdown 対応