Flutter 2025: Xây dựng pipeline kiểm thử năm chiều từ unit test đến giám sát production

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ầngCông cụTốc độMục tiêu
Unittest< 10 msLogic nguyên thủy
Widgetflutter_test≈ 100 msRender & interaction
Integrationintegration_test≈ 2 sLuồng đa màn hình
E2EMaestro / Appium10 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ùng Clock giả.
  • ❌ 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

  1. Pull request → unit & widget test (GitHub Actions).
  2. Merge vào mainintegration test trên máy ảo & thiết bị thật.
  3. Tag release → golden diff + E2E smoke.
  4. 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 = 0giảm 50 % khiếu nại.

Thẻ: Flutter Unit Test Widget Test Integration Test Golden Test

Đăng vào ngày 15 tháng 8 lúc 11:50