templates/skills/flutter-testing/SKILL.md
# Flutter Testing Patterns Comprehensive testing patterns for Flutter applications. > **Template Usage:** Customize for your state management (Riverpod, Bloc, Provider) and testing preferences. ## Test Structure ``` test/ ├── unit/ # Pure Dart logic tests │ ├── models/ │ ├── repositories/ │ └── utils/ ├── widget/ # Widget tests │ ├── screens/ │ └── components/ ├── golden/ # Visual regression tests │ └── screenshots/ ├── integra
npx skillsauth add javeedishaq/ai-workflow-orchestrator templates/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.
Comprehensive testing patterns for Flutter applications.
Template Usage: Customize for your state management (Riverpod, Bloc, Provider) and testing preferences.
test/
├── unit/ # Pure Dart logic tests
│ ├── models/
│ ├── repositories/
│ └── utils/
├── widget/ # Widget tests
│ ├── screens/
│ └── components/
├── golden/ # Visual regression tests
│ └── screenshots/
├── integration/ # Full app tests
│ └── flows/
├── fixtures/ # Test data
│ ├── json/
│ └── mocks/
└── helpers/ # Test utilities
├── pump_app.dart
├── mocks.dart
└── finders.dart
// test/unit/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/modules/user/domain/models/user.dart';
void main() {
group('User', () {
test('fromJson creates valid User', () {
final json = {
'id': '123',
'email': '[email protected]',
'name': 'Test User',
'is_verified': true,
};
final user = User.fromJson(json);
expect(user.id, '123');
expect(user.email, '[email protected]');
expect(user.name, 'Test User');
expect(user.isVerified, true);
});
test('toJson produces valid JSON', () {
const user = User(
id: '123',
email: '[email protected]',
name: 'Test User',
);
final json = user.toJson();
expect(json['id'], '123');
expect(json['email'], '[email protected]');
});
test('copyWith creates modified copy', () {
const user = User(id: '123', email: '[email protected]');
final updated = user.copyWith(name: 'New Name');
expect(updated.name, 'New Name');
expect(updated.id, user.id);
expect(updated.email, user.email);
});
test('equality works correctly', () {
const user1 = User(id: '123', email: '[email protected]');
const user2 = User(id: '123', email: '[email protected]');
const user3 = User(id: '456', email: '[email protected]');
expect(user1, equals(user2));
expect(user1, isNot(equals(user3)));
});
});
}
// test/unit/repositories/user_repository_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class MockSupabaseClient extends Mock implements SupabaseClient {}
class MockSupabaseQueryBuilder extends Mock implements SupabaseQueryBuilder {}
class MockPostgrestFilterBuilder extends Mock implements PostgrestFilterBuilder {}
void main() {
late MockSupabaseClient mockClient;
late UserRepository repository;
setUp(() {
mockClient = MockSupabaseClient();
repository = SupabaseUserRepository(mockClient);
});
group('UserRepository', () {
group('getUser', () {
test('returns user when found', () async {
final mockQueryBuilder = MockSupabaseQueryBuilder();
final mockFilterBuilder = MockPostgrestFilterBuilder();
when(() => mockClient.from('users')).thenReturn(mockQueryBuilder);
when(() => mockQueryBuilder.select()).thenReturn(mockFilterBuilder);
when(() => mockFilterBuilder.eq('id', '123')).thenReturn(mockFilterBuilder);
when(() => mockFilterBuilder.maybeSingle()).thenAnswer(
(_) async => {'id': '123', 'email': '[email protected]'},
);
final user = await repository.getUser('123');
expect(user, isNotNull);
expect(user!.id, '123');
});
test('returns null when not found', () async {
// Setup mocks to return null...
final user = await repository.getUser('nonexistent');
expect(user, isNull);
});
test('throws on network error', () async {
when(() => mockClient.from('users')).thenThrow(
PostgrestException(message: 'Network error'),
);
expect(
() => repository.getUser('123'),
throwsA(isA<NetworkException>()),
);
});
});
});
}
// test/widget/components/user_avatar_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/shared/widgets/user_avatar.dart';
void main() {
group('UserAvatar', () {
testWidgets('displays initials when no image', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: UserAvatar(name: 'John Doe'),
),
),
);
expect(find.text('JD'), findsOneWidget);
});
testWidgets('displays image when provided', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: UserAvatar(
name: 'John Doe',
imageUrl: 'https://example.com/avatar.jpg',
),
),
),
);
expect(find.byType(CircleAvatar), findsOneWidget);
});
testWidgets('respects size parameter', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: UserAvatar(name: 'John', size: 100),
),
),
);
final avatar = tester.widget<CircleAvatar>(find.byType(CircleAvatar));
expect(avatar.radius, 50); // radius = size / 2
});
});
}
// test/widget/screens/login_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockAuthNotifier extends Mock implements AuthNotifier {}
void main() {
late MockAuthNotifier mockAuthNotifier;
setUp(() {
mockAuthNotifier = MockAuthNotifier();
});
Widget buildTestWidget() {
return ProviderScope(
overrides: [
authProvider.overrideWith(() => mockAuthNotifier),
],
child: const MaterialApp(
home: LoginScreen(),
),
);
}
group('LoginScreen', () {
testWidgets('displays email and password fields', (tester) async {
when(() => mockAuthNotifier.build()).thenReturn(const AuthState.initial());
await tester.pumpWidget(buildTestWidget());
expect(find.byKey(const Key('email_field')), findsOneWidget);
expect(find.byKey(const Key('password_field')), findsOneWidget);
});
testWidgets('shows validation error for invalid email', (tester) async {
when(() => mockAuthNotifier.build()).thenReturn(const AuthState.initial());
await tester.pumpWidget(buildTestWidget());
await tester.enterText(find.byKey(const Key('email_field')), 'invalid');
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid email'), findsOneWidget);
});
testWidgets('calls signIn on valid submission', (tester) async {
when(() => mockAuthNotifier.build()).thenReturn(const AuthState.initial());
when(() => mockAuthNotifier.signIn(any(), any())).thenAnswer((_) async {});
await tester.pumpWidget(buildTestWidget());
await tester.enterText(
find.byKey(const Key('email_field')),
'[email protected]',
);
await tester.enterText(
find.byKey(const Key('password_field')),
'password123',
);
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pumpAndSettle();
verify(() => mockAuthNotifier.signIn('[email protected]', 'password123')).called(1);
});
testWidgets('shows loading indicator during authentication', (tester) async {
when(() => mockAuthNotifier.build()).thenReturn(const AuthState.loading());
await tester.pumpWidget(buildTestWidget());
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('shows error message on auth failure', (tester) async {
when(() => mockAuthNotifier.build()).thenReturn(
const AuthState.error('Invalid credentials'),
);
await tester.pumpWidget(buildTestWidget());
expect(find.text('Invalid credentials'), findsOneWidget);
});
});
}
// test/golden/user_profile_golden_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('UserProfile Golden Tests', () {
testGoldens('renders correctly on different devices', (tester) async {
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(devices: [
Device.phone,
Device.iphone11,
Device.tabletPortrait,
])
..addScenario(
name: 'default',
widget: const UserProfileScreen(userId: '123'),
)
..addScenario(
name: 'loading',
widget: const UserProfileScreen(userId: '123', isLoading: true),
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'user_profile_multi_device');
});
testGoldens('handles dark mode', (tester) async {
await tester.pumpWidgetBuilder(
const UserProfileScreen(userId: '123'),
wrapper: materialAppWrapper(
theme: ThemeData.dark(),
),
);
await screenMatchesGolden(tester, 'user_profile_dark');
});
});
}
// Update golden files:
// flutter test --update-goldens
// 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();
group('End-to-end Tests', () {
testWidgets('complete login flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Verify on login screen
expect(find.text('Sign In'), findsOneWidget);
// Enter credentials
await tester.enterText(
find.byKey(const Key('email_field')),
'[email protected]',
);
await tester.enterText(
find.byKey(const Key('password_field')),
'password123',
);
// Submit
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pumpAndSettle();
// Verify navigation to home
expect(find.text('Welcome'), findsOneWidget);
});
testWidgets('navigation between screens', (tester) async {
app.main();
await tester.pumpAndSettle();
// Login first...
// Navigate to profile
await tester.tap(find.byIcon(Icons.person));
await tester.pumpAndSettle();
expect(find.text('Profile'), findsOneWidget);
// Navigate back
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
});
});
}
// Run integration tests:
// flutter test integration_test
// test/helpers/pump_app.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
extension PumpApp on WidgetTester {
Future<void> pumpApp(
Widget widget, {
List<Override> overrides = const [],
ThemeData? theme,
}) async {
await pumpWidget(
ProviderScope(
overrides: overrides,
child: MaterialApp(
theme: theme ?? ThemeData.light(),
home: widget,
),
),
);
}
Future<void> pumpAppWithRouter(
GoRouter router, {
List<Override> overrides = const [],
}) async {
await pumpWidget(
ProviderScope(
overrides: overrides,
child: MaterialApp.router(
routerConfig: router,
),
),
);
}
}
// test/helpers/mocks.dart
import 'package:mocktail/mocktail.dart';
class MockUserRepository extends Mock implements UserRepository {}
class MockAuthRepository extends Mock implements AuthRepository {}
class MockSupabaseClient extends Mock implements SupabaseClient {}
// Fake classes for registerFallbackValue
class FakeUser extends Fake implements User {}
class FakeCreateUserInput extends Fake implements CreateUserInput {}
void setupMocks() {
registerFallbackValue(FakeUser());
registerFallbackValue(FakeCreateUserInput());
}
// test/helpers/finders.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Finder findByKeyString(String key) => find.byKey(Key(key));
Finder findTextButton(String text) => find.widgetWithText(TextButton, text);
Finder findElevatedButton(String text) => find.widgetWithText(ElevatedButton, text);
Finder findTextField(String label) => find.widgetWithText(TextField, label);
extension WidgetTesterX on WidgetTester {
Future<void> enterTextByKey(String key, String text) async {
await enterText(findByKeyString(key), text);
}
Future<void> tapByKey(String key) async {
await tap(findByKeyString(key));
}
}
// test/fixtures/user_fixtures.dart
class UserFixtures {
static const testUser = User(
id: 'test-user-id',
email: '[email protected]',
name: 'Test User',
isVerified: true,
);
static const adminUser = User(
id: 'admin-user-id',
email: '[email protected]',
name: 'Admin User',
isVerified: true,
);
static List<User> userList([int count = 10]) {
return List.generate(
count,
(i) => User(
id: 'user-$i',
email: '[email protected]',
name: 'User $i',
),
);
}
static Map<String, dynamic> userJson = {
'id': 'test-user-id',
'email': '[email protected]',
'name': 'Test User',
'is_verified': true,
};
}
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Run specific test file
flutter test test/unit/models/user_test.dart
# Run tests matching pattern
flutter test --name "UserRepository"
# Run integration tests
flutter test integration_test
# Update golden files
flutter test --update-goldens
# Run with verbose output
flutter test --reporter expanded
tools
# Test Patterns Testing patterns for reliable, maintainable, and fast tests. > **Template Usage:** Customize for your test framework (Vitest, Jest, Playwright, etc.) and assertion library. ## Test Structure ```typescript // user.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { userService } from '@/services/user.service'; import { createTestUser, cleanupTestData } from '@/tests/helpers'; describe('UserService', () => { let testUserId: string; befor
tools
# State Management Patterns Client-side state management patterns for modern applications. > **Template Usage:** Customize for your state library (React Query, Zustand, Jotai, Redux, etc.). ## State Categories | Type | Description | Solution | |------|-------------|----------| | **Server State** | Data from API/database | React Query, SWR | | **Client State** | UI state, user preferences | Zustand, Jotai, useState | | **Form State** | Form inputs, validation | React Hook Form, Formik | | **U
development
# Service Patterns Service layer patterns for clean architecture with proper error handling, logging, and type safety. > **Template Usage:** Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution. ## Result Type Pattern Never throw exceptions from services. Always return a Result type. ```typescript // lib/result.ts export type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; export function ok<T>(data: T): Result<T, never> { r
testing
# Row-Level Security Patterns Database security patterns for multi-tenant and user-scoped data. > **Template Usage:** Customize for your database (PostgreSQL, Supabase, etc.) and auth system. ## RLS Fundamentals ### Enable RLS on Tables ```sql -- Enable RLS (required before policies take effect) ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Force RLS for table owners too (recommended) ALTER TABLE