skills/flutter-testing/SKILL.md
Flutter の unit / widget / integration test の戦略・実装パターン・ベストプラクティス
npx skillsauth add oto1720/claude-agents-skills flutter-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
/\
/ \
/ E2E \ 統合テスト(少数・遅い)
/--------\
/ Widget \ Widget テスト(中程度)
/------------\
/ Unit Tests \ Unit テスト(多数・高速)
/________________\
推奨比率: Unit 70% / Widget 20% / Integration 10%
void main() {
late UserRepository sut;
late MockUserApiClient mockApiClient;
late MockUserDao mockDao;
setUp(() {
mockApiClient = MockUserApiClient();
mockDao = MockUserDao();
sut = UserRepositoryImpl(
apiClient: mockApiClient,
dao: mockDao,
);
});
group('getUser', () {
test('APIから正常取得できる', () async {
// Arrange
final dto = UserDto(id: '1', name: 'Alice');
when(() => mockApiClient.getUser('1')).thenAnswer((_) async => dto);
// Act
final result = await sut.getUser('1');
// Assert
expect(result, User(id: '1', name: 'Alice'));
verify(() => mockApiClient.getUser('1')).called(1);
});
test('APIエラー時はキャッシュから返す', () async {
// Arrange
when(() => mockApiClient.getUser('1')).thenThrow(NetworkException());
when(() => mockDao.getUser('1')).thenAnswer(
(_) async => UserEntity(id: '1', name: 'Alice (cached)'),
);
// Act
final result = await sut.getUser('1');
// Assert
expect(result.name, 'Alice (cached)');
});
});
}
void main() {
late ProviderContainer container;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
);
});
tearDown(() => container.dispose());
test('loadUser 成功時に AsyncData が発行される', () async {
// Arrange
final user = User(id: '1', name: 'Alice');
when(() => mockRepository.getUser('1')).thenAnswer((_) async => user);
// Act
final notifier = container.read(userNotifierProvider('1').notifier);
// Assert: build() が呼ばれた後
await container.read(userNotifierProvider('1').future);
expect(
container.read(userNotifierProvider('1')),
isA<AsyncData<User>>().having((s) => s.value, 'value', user),
);
});
}
void main() {
late UserBloc bloc;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
bloc = UserBloc(userRepository: mockRepository);
});
tearDown(() => bloc.close());
blocTest<UserBloc, UserState>(
'LoadUser イベントでユーザーが取得される',
build: () => bloc,
setUp: () {
when(() => mockRepository.getUser('1'))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
},
act: (bloc) => bloc.add(const LoadUser('1')),
expect: () => [
const UserLoading(),
UserLoaded(User(id: '1', name: 'Alice')),
],
);
blocTest<UserBloc, UserState>(
'APIエラー時に UserError が発行される',
build: () => bloc,
setUp: () {
when(() => mockRepository.getUser('1'))
.thenThrow(Exception('Network error'));
},
act: (bloc) => bloc.add(const LoadUser('1')),
expect: () => [
const UserLoading(),
isA<UserError>(),
],
);
}
void main() {
group('UserCard', () {
testWidgets('ユーザー名が表示される', (tester) async {
// Arrange
const user = User(id: '1', name: 'Alice', role: 'Admin');
// Act
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: UserCard(user: user),
),
),
);
// Assert
expect(find.text('Alice'), findsOneWidget);
expect(find.text('Admin'), findsOneWidget);
});
testWidgets('タップで onTap が呼ばれる', (tester) async {
var tapped = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserCard(
user: const User(id: '1', name: 'Alice'),
onTap: () => tapped = true,
),
),
),
);
await tester.tap(find.byType(UserCard));
expect(tapped, isTrue);
});
});
}
testWidgets('ユーザー取得中はローディングが表示される', (tester) async {
final container = ProviderContainer(
overrides: [
// Provider を差し替えてローディング状態を再現
userNotifierProvider('1').overrideWith(
() => AsyncNotifier.fromValue(const AsyncLoading<User>()),
),
],
);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const MaterialApp(home: UserScreen(userId: '1')),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('UserCard のゴールデンテスト', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: UserCard(user: User(id: '1', name: 'Alice')),
),
),
);
await expectLater(
find.byType(UserCard),
matchesGoldenFile('goldens/user_card.png'),
);
});
// integration_test/app_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('ログインからホーム画面への遷移', (tester) async {
app.main();
await tester.pumpAndSettle();
// ログイン画面が表示される
expect(find.byType(LoginScreen), findsOneWidget);
// メールとパスワードを入力
await tester.enterText(find.byKey(const Key('email_field')), '[email protected]');
await tester.enterText(find.byKey(const Key('password_field')), 'password');
// ログインボタンをタップ
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 3));
// ホーム画面に遷移
expect(find.byType(HomeScreen), findsOneWidget);
});
}
# 全テスト
flutter test
# カバレッジ付き
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
# 特定ファイル
flutter test test/features/user/user_notifier_test.dart
# ゴールデンテスト更新
flutter test --update-goldens
# Integration test
flutter test integration_test/app_test.dart
development
プロジェクト全体の技術構成図(アーキテクチャダイアグラム)を自動生成するスキル。リポジトリやプロジェクトのコードベースを解析し、使用技術・依存関係・レイヤー構造・データフロー・インフラ構成を可視化したMermaid/SVG/HTML図を生成する。「技術構成図を作って」「アーキテクチャ図」「システム構成を可視化」「プロジェクトの全体像」「tech stack diagram」などのリクエストで必ずこのスキルを使用すること。プロジェクトの理解・オンボーディング資料・ドキュメント作成にも活用できる。
testing
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
development
セキュリティ観点でコードを精査し、脆弱性・リスクをレポートする。 以下のトリガーで自動発動: - 「セキュリティレビューして」「脆弱性チェック」「セキュリティ問題ない?」 - 「認証コードを確認して」「APIキーや秘密情報が漏れていないか確認して」 - /security-review [ファイルパス]
tools
PRやコミットの差分をレビューして、マージ可否の判断と指摘事項を出力する。 以下のトリガーで自動発動: - 「PRレビューして」「このPRどう思う?」「マージしても大丈夫?」 - 「差分をレビューして」「コミット内容を確認して」 - /pr-review [ブランチ名 or コミットハッシュ]