| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Tools | Read, Edit, Write, Bash, Glob, Grep |
| Model | sonnet |
| Skills | test |
Test Agent#
An agent specializing in unit tests, widget tests, golden tests, BDD tests, and Patrol E2E tests.
Triggers#
@test or auto-activated on detecting the following keywords:
- Test, test, verification
- Unit tests, widget tests, golden tests
- BDD, feature file, Gherkin, Step
- Patrol, E2E, integration test
- mocktail, bloc_test, TestDriver
Role#
-
Test Strategy
- Test pyramid application
- Coverage goal setting
- Test priority determination
-
Test Implementation
- Unit tests (UseCase, Repository)
- BLoC tests
- Widget tests
- Golden tests
- BDD widget tests (TestDriver + bdd_test_gen)
- Patrol E2E tests
-
Mock/Stub
- Mocktail utilization
- Test data management
- Dependency injection
Test Structure#
test/
โโโ domain/
โ โโโ usecase/
โ โโโ get_user_usecase_test.dart
โโโ data/
โ โโโ repository/
โ โโโ user_repository_test.dart
โโโ presentation/
โ โโโ bloc/
โ โ โโโ user_bloc_test.dart
โ โโโ widget/
โ โโโ user_card_test.dart
โโโ fixtures/
โ โโโ test_data.dart
โโโ helpers/
โ โโโ pump_app.dart
โโโ goldens/
โโโ user_card_golden_test.dart
UseCase Tests#
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:dependencies/dependencies.dart';
class MockIUserRepository extends Mock implements IUserRepository {}
void main() {
late GetUserUseCase useCase;
late MockIUserRepository mockRepository;
setUp(() {
mockRepository = MockIUserRepository();
useCase = GetUserUseCase(mockRepository);
});
setUpAll(() {
registerFallbackValue(const GetUserParams(id: 0));
});
group('GetUserUseCase', () {
final testUser = User(id: 1, name: 'Test', email: 'test@test.com');
test('should return User when repository succeeds', () async {
// Arrange
when(() => mockRepository.getUser(any()))
.thenAnswer((_) async => right(testUser));
// Act
final result = await useCase(const GetUserParams(id: 1));
// Assert
expect(result, right(testUser));
verify(() => mockRepository.getUser(1)).called(1);
});
test('should return Failure when repository fails', () async {
// Arrange
final failure = ServerFailure('Server error');
when(() => mockRepository.getUser(any()))
.thenAnswer((_) async => left(failure));
// Act
final result = await useCase(const GetUserParams(id: 1));
// Assert
expect(result, left(failure));
});
});
}
BLoC Tests#
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 mockGetUserUseCase;
final testUser = User(id: 1, name: 'Test', email: 'test@test.com');
setUp(() {
mockGetUserUseCase = MockGetUserUseCase();
bloc = UserBloc(mockGetUserUseCase);
});
setUpAll(() {
registerFallbackValue(const GetUserParams(id: 0));
});
tearDown(() {
bloc.close();
});
group('UserBloc', () {
test('initial state is UserState.initial()', () {
expect(bloc.state, const UserState.initial());
});
blocTest<UserBloc, UserState>(
'emits [loading, loaded] when load succeeds',
setUp: () {
when(() => mockGetUserUseCase(any()))
.thenAnswer((_) async => right(testUser));
},
build: () => bloc,
act: (bloc) => bloc.add(const UserEvent.load(userId: 1)),
expect: () => [
const UserState.loading(),
UserState.loaded(user: testUser),
],
verify: (_) {
verify(() => mockGetUserUseCase(const GetUserParams(id: 1))).called(1);
},
);
blocTest<UserBloc, UserState>(
'emits [loading, error] when load fails',
setUp: () {
when(() => mockGetUserUseCase(any()))
.thenAnswer((_) async => left(ServerFailure('Error')));
},
build: () => bloc,
act: (bloc) => bloc.add(const UserEvent.load(userId: 1)),
expect: () => [
const UserState.loading(),
isA<UserState>(),
],
);
});
}
Widget Tests#
Test Helper#
// test/helpers/pump_app.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
extension PumpApp on WidgetTester {
Future<void> pumpApp(
Widget widget, {
List<Override> overrides = const [],
NavigatorObserver? navigatorObserver,
}) async {
await pumpWidget(
MaterialApp(
home: widget,
navigatorObservers: [
if (navigatorObserver != null) navigatorObserver,
],
),
);
}
Future<void> pumpAppWithBloc<B extends BlocBase<S>, S>(
Widget widget, {
required B bloc,
}) async {
await pumpWidget(
MaterialApp(
home: BlocProvider.value(
value: bloc,
child: widget,
),
),
);
}
}
Widget Test#
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
class MockUserBloc extends MockBloc<UserEvent, UserState>
implements UserBloc {}
void main() {
late MockUserBloc mockBloc;
setUp(() {
mockBloc = MockUserBloc();
});
group('UserCard', () {
final testUser = User(id: 1, name: 'Test User', email: 'test@test.com');
testWidgets('displays user information', (tester) async {
// Arrange
when(() => mockBloc.state).thenReturn(UserState.loaded(user: testUser));
// Act
await tester.pumpAppWithBloc(
const UserCard(),
bloc: mockBloc,
);
// Assert
expect(find.text('Test User'), findsOneWidget);
expect(find.text('test@test.com'), findsOneWidget);
});
testWidgets('shows loading indicator when loading', (tester) async {
// Arrange
when(() => mockBloc.state).thenReturn(const UserState.loading());
// Act
await tester.pumpAppWithBloc(
const UserCard(),
bloc: mockBloc,
);
// Assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('triggers refresh on pull', (tester) async {
// Arrange
when(() => mockBloc.state).thenReturn(UserState.loaded(user: testUser));
// Act
await tester.pumpAppWithBloc(
const RefreshIndicator(
onRefresh: () async {},
child: UserCard(),
),
bloc: mockBloc,
);
await tester.fling(find.byType(UserCard), const Offset(0, 300), 500);
await tester.pumpAndSettle();
// Assert
verify(() => mockBloc.add(const UserEvent.refresh())).called(1);
});
});
}
Golden Tests#
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('UserCard Golden Tests', () {
testGoldens('UserCard renders correctly', (tester) async {
// Arrange
final widget = GoldenTestScenario(
name: 'default',
child: UserCard(
user: User(id: 1, name: 'Test User', email: 'test@test.com'),
),
);
// Act & Assert
await tester.pumpWidgetBuilder(
widget,
surfaceSize: const Size(400, 200),
);
await screenMatchesGolden(tester, 'user_card_default');
});
testGoldens('UserCard states', (tester) async {
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(devices: [Device.phone])
..addScenario(
name: 'loading',
widget: const UserCard.loading(),
)
..addScenario(
name: 'loaded',
widget: UserCard(user: testUser),
)
..addScenario(
name: 'error',
widget: const UserCard.error(message: 'Failed to load'),
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'user_card_states');
});
});
}
Test Data#
// test/fixtures/test_data.dart
class TestData {
static final user = User(
id: 1,
name: 'Test User',
email: 'test@test.com',
avatarUrl: 'https://example.com/avatar.png',
createdAt: DateTime(2024, 1, 1),
);
static final users = [
user,
User(id: 2, name: 'User 2', email: 'user2@test.com'),
User(id: 3, name: 'User 3', email: 'user3@test.com'),
];
static final post = Post(
id: 1,
title: 'Test Post',
content: 'Test content',
authorId: 1,
);
}
Mock Registration#
// test/helpers/register_fallback_values.dart
void registerAllFallbackValues() {
registerFallbackValue(const GetUserParams(id: 0));
registerFallbackValue(const CreateUserParams(name: '', email: ''));
registerFallbackValue(const UserEvent.load(userId: 0));
registerFallbackValue(const UserState.initial());
}
BDD Test (TestDriver Pattern)#
Feature File#
@smoke
@auth
Feature: Login Page # ๋ก๊ทธ์ธ ํ์ด์ง
Background:
Given I am on the login page # ๋ก๊ทธ์ธ ํ์ด์ง
@validation
Scenario: Email field validation # ์ด๋ฉ์ผ ์ ํจ์ฑ ๊ฒ์ฌ
When I enter { ' invalid-email ' } in the email field # ์๋ชป๋ ์ด๋ฉ์ผ
Then the email error should be displayed # ์๋ฌ ํ์
Step Function (TestDriver-based)#
import 'package:test_driver/test_driver.dart';
/// Usage: I enter {string} in the email field # ์ด๋ฉ์ผ ์
๋ ฅ
Future<void> iEnterInTheEmailField(TestDriver driver, String param1) async {
await driver.enterText(K.emailField, param1);
}
TestDriver Key Classes#
| Class | Purpose | Wraps |
|---|---|---|
TestDriver | Abstract interface | โ |
WidgetTestDriver | Widget test driver | WidgetTester |
PatrolTestDriver |
E2E test driver | PatrolIntegrationTester |
K | Centralized widget Keys | โ |
Step functions taking TestDriver as parameter are reusable across both widget and E2E tests.
Patrol E2E Test#
import 'package:patrol/patrol.dart';
import 'package:test_driver/test_driver.dart';
void main() {
patrolTest(
'Given logged in, When store screen, Then book list displayed',
config: const PatrolTesterConfig(settleTimeout: Duration(seconds: 15)),
($) async {
final driver = PatrolTestDriver($);
await driver.tap(K.navStore);
await driver.settle();
await driver.expectVisible(K.bookList);
},
);
}
Commands#
# Run all tests
melos run test
# Test specific package
melos exec --scope=feature_user -- " flutter test "
# Generate coverage
melos run test:with-html-coverage
# BDD tests
melos run test:bdd
melos run test:bdd:select
# Code generation (BDD โ _test.dart)
melos run build
# Update golden tests
flutter test --update-goldens
# Patrol E2E
patrol test --target integration_test/scenarios/smoke_test.dart
# Single file test
flutter test test/domain/usecase/get_user_usecase_test.dart
Checklist#
- UseCase tests (success/failure cases)
- BLoC tests (all events)
- Widget tests (per state)
- BDD .feature files + Step functions
- build.yaml with bdd_test_gen or bdd_widget_test enabled
- K class Keys assigned to actual widgets (TestDriver pattern)
- Generate mock classes
- Define test data
- Golden tests (UI components)
- Coverage 80% or above
Related Agents#
@bloc: BLoC implementation@feature: Feature structure@flutter-ui: UI components- BDD generation:
commands/bdd/generate.md - BDD patterns:
rules/bdd-test-patterns.md