Chuyển đến nội dung chính

第 19 課:Flutter 中的測試

使用測試包進行單元測試。小工具測試、查找器、匹配器。集成測試。用無酒精雞尾酒/mockito 進行嘲笑。黃金測試。測試覆蓋率、CI 整合。

💻 程式設計 — 第 19 課 第 19 課:Flutter 中的測試

Flutter 和 Dart:從基礎到高級

第 6 部分:測試、CI/CD 和生產

亞洲開發網

1. 單元測試

// test/product_repository_test.dart
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockApiClient extends Mock implements ApiClient {}

void main() {
  late ProductRepository repository;
  late MockApiClient mockApi;

  setUp(() {
    mockApi = MockApiClient();
    repository = ProductRepository(mockApi);
  });

  group('getProducts', () {
    test('should return list of products on success', () async {
      final products = [Product(id: '1', name: 'Laptop', price: 999)];
      when(() => mockApi.getProducts()).thenAnswer((_) async => products);

      final result = await repository.getProducts();

      expect(result, isA<Right>());
      result.fold(
        (failure) => fail('Should not be failure'),
        (data) => expect(data, equals(products)),
      );
      verify(() => mockApi.getProducts()).called(1);
    });

    test('should return ServerFailure on exception', () async {
      when(() => mockApi.getProducts()).thenThrow(ServerException('Server error'));

      final result = await repository.getProducts();

      expect(result, isA<Left>());
      result.fold(
        (failure) => expect(failure, isA<ServerFailure>()),
        (_) => fail('Should be failure'),
      );
    });
  });
}

2. 小部件測試

// test/widgets/product_card_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('ProductCard displays product info', (tester) async {
    final product = Product(id: '1', name: 'Laptop', price: 999);

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(body: ProductCard(product: product)),
      ),
    );

    expect(find.text('Laptop'), findsOneWidget);
    expect(find.text('\$999.00'), findsOneWidget);
  });

  testWidgets('ProductCard tap navigates to detail', (tester) async {
    final product = Product(id: '1', name: 'Laptop', price: 999);
    var tapped = false;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: ProductCard(
            product: product,
            onTap: () => tapped = true,
          ),
        ),
      ),
    );

    await tester.tap(find.byType(ProductCard));
    await tester.pumpAndSettle();

    expect(tapped, isTrue);
  });
}

3. 測試塊/肘

import 'package:bloc_test/bloc_test.dart';

class MockGetProducts extends Mock implements GetProducts {}

void main() {
  late ProductBloc bloc;
  late MockGetProducts mockGetProducts;

  setUp(() {
    mockGetProducts = MockGetProducts();
    bloc = ProductBloc(getProducts: mockGetProducts);
  });

  blocTest<ProductBloc, ProductState>(
    'emits [Loading, Loaded] when FetchProducts is added',
    build: () {
      when(() => mockGetProducts(any()))
          .thenAnswer((_) async => Right([Product(id: '1', name: 'Laptop', price: 999)]));
      return bloc;
    },
    act: (bloc) => bloc.add(FetchProducts()),
    expect: () => [
      const ProductLoading(),
      isA<ProductLoaded>(),
    ],
  );
}

4. 整合測試

// integration_test/app_test.dart
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Full login flow', (tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();

    // Nhập email
    await tester.enterText(find.byKey(const Key('email_field')), '[email protected]');
    // Nhập password
    await tester.enterText(find.byKey(const Key('password_field')), 'password123');
    // Tap login
    await tester.tap(find.byKey(const Key('login_button')));
    await tester.pumpAndSettle();

    // Verify navigation to home
    expect(find.text('Home'), findsOneWidget);
  });
}
# Run tests
flutter test
flutter test --coverage
flutter test integration_test/

下一篇: 效能與調試。