1. Tại sao chỉ "test UI" là sai lầm nghiêm trọng?
Theo báo cáo Mobile Quality 2024, 71 % lỗi nghiêm trọng trên production bắt nguồn từ các kịch bản biên chưa được bao phủ. Flutter đã tích hợp sẵn flutter test, integration_test, golden_toolkit vào DevTools, nhưng nếu thiếu chiến lược phân tầng, bạn vẫn rơi vào "test cho có".
2. Kim tự tháp kiểm thử phiên bản 2025
┌──────────────┐ E2E (5 %) – chỉ chạy journey chính
│ E2E Tests │
├──────────────┤ Integration (15 %) – kiểm giao tiếp giữa module
│ Integration │
├──────────────┤ Widget + Unit (80 %) – logic & UI state
│ Widget/Unit │
└──────────────┘
| Tầng | Công cụ | Tốc độ | Mục tiêu |
|---|---|---|---|
| Unit | test | < 10 ms | Logic nguyên thủy |
| Widget | flutter_test | ≈ 100 ms | Render & interaction |
| Integration | integration_test | ≈ 2 s | Luồng đa màn hình |
| E2E | Maestro / Appium | 10 s+ | Thiết bị thật |
3. Kiến trúc "dễ test" với Clean Architecture + Riverpod
lib/
├── core/ # entity, failure
├── domain/ # use-case, repository interface
├── data/ # repository impl, remote/local
└── presentation/ # UI + state (Riverpod)
Điểm mấu chốt: domain không import Flutter nên unit test chạy trên Dart VM thuần túy.
4. Unit test – 100 % logic Dart
// domain/usecase/calculate_discount.dart
double calcDiscount(double price, double rate) {
if (rate < 0 || rate > 1) throw ArgumentError('rate invalid');
return price * (1 - rate);
}
// test/calculate_discount_test.dart
void main() {
group('calcDiscount', () {
test('normal', () => expect(calcDiscount(100, 0.2), 80));
test('boundary 0', () => expect(calcDiscount(100, 0), 100));
test('boundary 1', () => expect(calcDiscount(100, 1), 0));
test('invalid rate', () => expect(() => calcDiscount(100, 1.5), throwsArgumentError));
});
}
5. Widget test – kiểm tra trạng thái UI
testWidgets('Hiển thị username sau khi load', (t) async {
await t.pumpWidget(
ProviderScope(
overrides: [userProvider.overrideWithValue(AsyncValue.data('Alice'))],
child: const MaterialApp(home: ProfilePage()),
),
);
expect(find.text('Alice'), findsOneWidget);
});
6. Integration test – luồng đăng nhập hoàn chỉnh
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Đăng nhập thành công', (t) async {
await t.pumpWidget(const MyApp());
await t.enterText(find.byKey(const Key('email')), 'a@b.com');
await t.enterText(find.byKey(const Key('pass')), '123456');
await t.tap(find.text('Đăng nhập'));
await t.pumpAndSettle();
expect(find.text('Chào mừng'), findsOneWidget);
});
}
Chạy trên Firebase Test Lab để đảm bảo nhiều thiết bị Android/iOS.
7. Visual regression – chặn thay đổi UI không mong muốn
testWidgets('Home screen golden', (t) async {
await t.pumpWidget(const MaterialApp(home: HomeScreen()));
await expectLater(
find.byType(HomeScreen),
matchesGoldenFile('goldens/home_light.png'),
);
});
Dùng golden_toolkit sinh ảnh cho cả dark-mode, 3 kích thước màn hình.
8. Giám sát production – A/B & rollback tự động
final layout = FirebaseRemoteConfig.instance.getString('home_layout');
return layout == 'v2' ? const HomeV2() : const HomeV1();
Sentry theo dõi crash; nếu lỗi > 0.5 % trên journey chính, Fastlane tự rollback.
9. Anti-pattern checklist
- ❌ Test dùng
DateTime.now()→ dùngClockgiả. - ❌ E2E query DB → chỉ assert trạng thái client.
- ❌ Quên
pumpAndSettle()→ false positive. - ❌ Dữ liệu test không reset → flaky.
10. Pipeline tự động hoàn chỉnh
- Pull request → unit & widget test (GitHub Actions).
- Merge vào
main→ integration test trên máy ảo & thiết bị thật. - Tag release → golden diff + E2E smoke.
- Phát hành → remote config A/B + crash monitoring.
Chỉ cần 2 tuần sprint, bạn có thể release với P0 = 0 và giảm 50 % khiếu nại.