flutter-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

flutter-automated-testing

flutter-automated-testing

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.
为Flutter应用生成、配置和调试自动化测试,涵盖单元测试、Widget测试、集成测试和插件测试。分析架构组件(例如MVVM层)以生成隔离的、基于mock的测试以及端到端设备测试。假定你使用标准的Flutter项目结构、已有的业务逻辑,并且熟悉Dart测试范式。

Instructions

使用说明

1. Determine Test Type (Decision Logic)

1. 确定测试类型(决策逻辑)

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."
使用以下决策树评估用户的目标代码,以确定合适的测试策略:
  • 如果要验证单个函数、方法、ViewModel或Repository: 实现单元测试(进入步骤2)。
  • 如果要验证单个widget的UI、布局或交互: 实现Widget测试(进入步骤3)。
  • 如果要验证完整的应用行为、路由或在设备上的性能: 实现集成测试(进入步骤4)。
  • 如果要验证平台特定的原生代码(MethodChannels): 实现插件测试(进入步骤5)。
请停下来询问用户: "我们今天要测试哪个具体的类、widget或流程?如果你还没有提供相关源代码,请提供一下。"

2. Implement Unit Tests (Logic & Architecture)

2. 实现单元测试(逻辑与架构)

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.
dart
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.
dart
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);
    });
  });
}
单元测试在不渲染UI的情况下验证逻辑。它们必须存放在
test/
目录下,且文件名以
_test.dart
结尾。
  • 针对ViewModel(UI层逻辑): 伪造repository依赖,不要依赖Flutter UI库。
dart
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);
    });
  });
}
  • 针对Repository(数据层逻辑): 伪造API客户端或本地数据库服务。
dart
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)

3. 实现Widget测试(UI组件)

Widget tests verify UI rendering and interaction. They must reside in the
test/
directory and use the
flutter_test
package.
  • Use
    WidgetTester
    to build the widget.
  • Use
    Finder
    to locate elements (
    find.text()
    ,
    find.byKey()
    ,
    find.byWidget()
    ).
  • Use
    Matcher
    to verify existence (
    findsOneWidget
    ,
    findsNothing
    ,
    findsNWidgets
    ).
dart
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);
  });
}
Widget测试验证UI渲染和交互。它们必须存放在
test/
目录下,且使用
flutter_test
包。
  • 使用
    WidgetTester
    构建widget。
  • 使用
    Finder
    定位元素(
    find.text()
    find.byKey()
    find.byWidget()
    )。
  • 使用
    Matcher
    验证存在性(
    findsOneWidget
    findsNothing
    findsNWidgets
    )。
dart
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)

4. 实现集成测试(端到端)

Integration tests run on real devices or emulators. They must reside in the
integration_test/
directory.
  • Ensure
    integration_test
    is in
    dev_dependencies
    in
    pubspec.yaml
    .
  • Initialize
    IntegrationTestWidgetsFlutterBinding
    .
dart
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);
    });
  });
}
集成测试在真实设备或模拟器上运行。它们必须存放在
integration_test/
目录下。
  • 确保
    pubspec.yaml
    dev_dependencies
    中包含
    integration_test
  • 初始化
    IntegrationTestWidgetsFlutterBinding
dart
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)

5. 实现插件测试(原生与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)
如果要测试插件,测试必须覆盖Dart和原生通信两部分。
  • Dart侧: mock平台通道,调用插件的公开API。
  • 原生侧: 指导用户在对应目录下编写原生测试:
    • Android:
      android/src/test/
      (JUnit)
    • iOS/macOS:
      example/ios/RunnerTests/
      (XCTest)
    • Linux/Windows:
      linux/test/
      (GoogleTest)

6. Validate and Fix (Feedback Loop)

6. 验证与修复(反馈循环)

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."
为用户提供运行生成的测试的准确命令:
  • 单元/Widget测试:
    flutter test test/your_test_file.dart
  • 集成测试:
    flutter test integration_test/your_test_file.dart
请停下来询问用户: "请使用上述命令运行测试并粘贴输出。如果测试失败,请提供堆栈跟踪,以便我分析失败原因并生成修复方案。"

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. Use
    tester.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.
  • 单一数据源: 不要在测试中重复状态。始终为外部依赖(Repositories、Services)使用伪造或mock对象,以隔离被测单元。
  • Widget中不要包含逻辑: 编写Widget测试时,假定Widget是「哑组件」。所有业务逻辑都应该通过ViewModel/Controller的单元测试进行验证。
  • 文件命名: 所有测试文件必须以
    _test.dart
    结尾。
  • Pump与PumpAndSettle的区别: 单帧推进时使用
    tester.pump()
    。仅当需要等待动画或异步UI更新完成时,才使用
    tester.pumpAndSettle()
  • 不可变性: 将测试数据模型视为不可变的。为状态变化创建新实例,而不是修改现有的mock数据。
  • 不要使用
    dart:mirrors
    Flutter不支持反射。依赖代码生成工具(例如
    build_runner
    mockito
    mocktail
    )来实现mock。