flutter-automated-testing#
한마디로#
이 스킬은 Flutter 앱이 제대로 동작하는지 자동으로 점검하는 "검사 로봇"을 만들어 줍니다. 사람이 매번 손으로 버튼을 눌러보며 확인하는 대신, 코드로 짜인 검사 항목이 알아서 앱을 두드려 보고 문제를 잡아냅니다. 마치 자동차가 출고되기 전 공장에서 거치는 품질 검사 라인과 같습니다.
무엇을·언제#
- 무엇을 해주나요: 앱의 기능이 의도대로 작동하는지 확인하는 자동 테스트를 만들고, 설정하고, 문제가 생기면 고쳐 줍니다.
-
검사 종류는 크게 네 가지입니다.
- 부품 검사(Unit Test): 계산 로직이나 데이터 처리 같은 작은 단위가 맞게 동작하는지 확인.
- 화면 검사(Widget Test): 버튼, 글자 등 화면 요소가 잘 보이고 눌러지면 반응하는지 확인.
- 전체 흐름 검사(Integration Test): 실제 기기에서 앱을 처음부터 끝까지 사용해 보며 확인.
- 네이티브 연동 검사(Plugin Test): 안드로이드/iOS의 기기 고유 기능과 잘 연결되는지 확인.
- 언제 작동하나요: Flutter 앱에 새 테스트를 추가하거나, 기존 테스트를 정리·구조화하거나, 테스트가 실패해서 원인을 찾아 고쳐야 할 때 사용합니다.
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| Unit Test | 함수나 데이터 처리 같은 가장 작은 단위가 맞게 동작하는지 보는 부품 검사 |
| Widget Test | 버튼·글자 등 화면 요소가 잘 보이고 반응하는지 보는 화면 검사 |
| Integration Test | 실제 기기에서 앱 전체 흐름을 처음부터 끝까지 돌려 보는 종합 검사 |
| Plugin Test | 안드로이드·iOS의 기기 고유 기능과 잘 연결되는지 보는 연동 검사 |
| Fake / Mock | 진짜 외부 시스템(서버, DB) 대신 쓰는 가짜 대역 — 검사 환경을 단순하게 만들어 줌 |
| ViewModel | 화면 뒤에서 데이터와 동작을 처리하는 "두뇌" 역할의 코드 |
| Repository | 데이터를 가져오고 저장하는 일을 맡은 창고 관리자 같은 코드 |
| pump / pumpAndSettle | 화면을 한 단계 갱신하거나(애니메이션이 끝날 때까지) 기다렸다가 결과를 확인하는 동작 |
Goal#
Generates, configures, and debugs automated tests for Flutter applications, encompassing unit, widget, integration, and plugin testing. Analyzes architectural components (such as MVVM layers) to produce isolated, mock-driven tests and end-to-end device tests. Assumes a standard Flutter project structure, existing business logic, and familiarity with Dart testing paradigms.
Instructions#
1. Determine Test Type (Decision Logic)#
Evaluate the user's target code to determine the appropriate testing strategy using the following decision tree:
- If verifying a single function, method, ViewModel, or Repository: Implement a Unit Test (Proceed to Step 2).
- If verifying a single widget's UI, layout, or interaction: Implement a Widget Test (Proceed to Step 3).
- If verifying complete app behavior, routing, or performance on a device: Implement an Integration Test (Proceed to Step 4).
- If verifying platform-specific native code (MethodChannels): Implement a Plugin Test (Proceed to Step 5).
STOP AND ASK THE USER: "Which specific class, widget, or flow are we testing today? Please provide the relevant source code if you haven't already."
2. Implement Unit Tests (Logic & Architecture)#
Unit tests verify logic without rendering UI. They must reside in the test/ directory and end with
_test.dart.
- For ViewModels (UI Layer Logic): Fake the repository dependencies. Do not rely on Flutter UI libraries.
import 'package:test/test.dart';
// Import your ViewModel and Fakes here
void main() {
group('HomeViewModel tests', () {
test('Load bookings successfully', () {
final viewModel = HomeViewModel(
bookingRepository: FakeBookingRepository()..createBooking(kBooking),
userRepository: FakeUserRepository(),
);
expect(viewModel.bookings.isNotEmpty, true);
});
});
}
- For Repositories (Data Layer Logic): Fake the API clients or local database services.
import 'package:test/test.dart';
// Import your Repository and Fakes here
void main() {
group('BookingRepositoryRemote tests', () {
late BookingRepository bookingRepository;
late FakeApiClient fakeApiClient;
setUp(() {
fakeApiClient = FakeApiClient();
bookingRepository = BookingRepositoryRemote(apiClient: fakeApiClient);
});
test('should get booking', () async {
final result = await bookingRepository.getBooking(0);
final booking = result.asOk.value;
expect(booking, kBooking);
});
});
}
3. Implement Widget Tests (UI Components)#
Widget tests verify UI rendering and interaction. They must reside in the test/ directory and use the
flutter_test package.
- Use
WidgetTesterto build the widget. -
Use
Finderto locate elements (find.text(),find.byKey(),find.byWidget()). -
Use
Matcherto verify existence (findsOneWidget,findsNothing,findsNWidgets).
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('HomeScreen displays title and handles tap', (WidgetTester tester) async {
// 1. Setup Fakes and ViewModel
final bookingRepository = FakeBookingRepository()..createBooking(kBooking);
final viewModel = HomeViewModel(
bookingRepository: bookingRepository,
userRepository: FakeUserRepository(),
);
// 2. Build the Widget tree
await tester.pumpWidget(
MaterialApp(
home: HomeScreen(viewModel: viewModel),
),
);
// 3. Finders
final titleFinder = find.text('Home');
final buttonFinder = find.byKey(const Key('increment_button'));
// 4. Assertions
expect(titleFinder, findsOneWidget);
// 5. Interactions
await tester.tap(buttonFinder);
await tester.pumpAndSettle(); // Wait for animations/state updates to finish
expect(find.text('1'), findsOneWidget);
});
}
4. Implement Integration Tests (End-to-End)#
Integration tests run on real devices or emulators. They must reside in the integration_test/
directory.
-
Ensure
integration_testis indev_dependenciesinpubspec.yaml. - Initialize
IntegrationTestWidgetsFlutterBinding.
import 'package:flutter/material.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 App Test', () {
testWidgets('Full flow: tap FAB and verify counter', (WidgetTester tester) async {
// Load the full app
app.main();
await tester.pumpAndSettle();
// Verify initial state
expect(find.text('0'), findsOneWidget);
// Find and tap the FAB
final fab = find.byKey(const ValueKey('increment'));
await tester.tap(fab);
// Trigger a frame
await tester.pumpAndSettle();
// Verify state change
expect(find.text('1'), findsOneWidget);
});
});
}
5. Implement Plugin Tests (Native & Dart)#
If testing a plugin, tests must cover both Dart and Native communication.
- Dart Side: Mock the platform channel and call the plugin's public API.
-
Native Side: Instruct the user to write native tests in the respective directories:
- Android:
android/src/test/(JUnit) - iOS/macOS:
example/ios/RunnerTests/(XCTest) - Linux/Windows:
linux/test/(GoogleTest)
- Android:
6. Validate and Fix (Feedback Loop)#
Provide the user with the exact command to run the generated test:
- Unit/Widget:
flutter test test/your_test_file.dart - Integration:
flutter test integration_test/your_test_file.dart
STOP AND ASK THE USER: "Please run the test using the command above and paste the output. If the test fails, provide the stack trace so I can analyze the failure and generate a fix."
Constraints#
- Single Source of Truth: Do not duplicate state in tests. Always use fakes or mocks for external dependencies (Repositories, Services) to isolate the unit under test.
- No Logic in Widgets: When writing widget tests, assume the widget is "dumb". All business logic should be tested via the ViewModel/Controller unit tests.
- File Naming: All test files MUST end with
_test.dart. -
Pump vs PumpAndSettle: Use
tester.pump()for single frame advances. Usetester.pumpAndSettle()strictly when waiting for animations or asynchronous UI updates to complete. - Immutability: Treat test data models as immutable. Create new instances for state changes rather than mutating existing mock data.
-
Do not use
dart:mirrors: Flutter does not support reflection. Rely on code generation (e.g.,build_runner,mockito,mocktail) for mocking.