Skip to content
On this page

Flutter测试策略

测试是保证Flutter应用质量的关键环节。Flutter提供了全面的测试支持,包括单元测试、小部件测试和集成测试。本章将详细介绍Flutter中的各种测试类型及其最佳实践。

测试概述

测试类型

Flutter应用测试主要包括以下几种类型:

  1. 单元测试:测试单个函数或类的功能
  2. 小部件测试:测试单个小部件的UI和交互
  3. 集成测试:测试多个组件协同工作的功能
  4. 端到端测试:测试完整应用流程

测试依赖

pubspec.yaml中添加测试依赖:

yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  integration_test:
    sdk: flutter
  mockito: ^5.4.0
  bloc_test: ^9.1.0
  fake_async: ^1.3.1

单元测试

单元测试用于测试应用程序的单个函数、方法或类。它们应该是隔离的,不依赖于外部系统。

基础单元测试

dart
// calculator.dart
class Calculator {
  int add(int a, int b) {
    return a + b;
  }

  int subtract(int a, int b) {
    return a - b;
  }

  double divide(int dividend, int divisor) {
    if (divisor == 0) {
      throw ArgumentError('Division by zero');
    }
    return dividend / divisor;
  }

  bool isPrime(int number) {
    if (number <= 1) return false;
    if (number == 2) return true;
    if (number.isEven) return false;

    for (int i = 3; i * i <= number; i += 2) {
      if (number % i == 0) return false;
    }
    return true;
  }
}

// calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'calculator.dart';

void main() {
  group('Calculator Tests', () {
    late Calculator calculator;

    setUp(() {
      calculator = Calculator();
    });

    test('add should return correct sum', () {
      // Given
      const a = 5;
      const b = 3;

      // When
      final result = calculator.add(a, b);

      // Then
      expect(result, equals(8));
    });

    test('subtract should return correct difference', () {
      // Given
      const a = 10;
      const b = 4;

      // When
      final result = calculator.subtract(a, b);

      // Then
      expect(result, equals(6));
    });

    test('divide should return correct quotient', () {
      // Given
      const dividend = 10;
      const divisor = 2;

      // When
      final result = calculator.divide(dividend, divisor);

      // Then
      expect(result, equals(5.0));
    });

    test('divide by zero should throw ArgumentError', () {
      // Given
      const dividend = 10;
      const divisor = 0;

      // When & Then
      expect(
        () => calculator.divide(dividend, divisor),
        throwsA(const TypeMatcher<ArgumentError>()),
      );
    });

    test('isPrime should return true for prime numbers', () {
      // Given
      const primeNumber = 7;

      // When
      final result = calculator.isPrime(primeNumber);

      // Then
      expect(result, isTrue);
    });

    test('isPrime should return false for non-prime numbers', () {
      // Given
      const nonPrimeNumber = 8;

      // When
      final result = calculator.isPrime(nonPrimeNumber);

      // Then
      expect(result, isFalse);
    });

    test('isPrime should return false for numbers <= 1', () {
      // Given
      const invalidNumber = -1;

      // When
      final result = calculator.isPrime(invalidNumber);

      // Then
      expect(result, isFalse);
    });
  });
}

异步单元测试

dart
// async_service.dart
class AsyncService {
  Future<String> fetchData(String url) async {
    // 模拟网络延迟
    await Future.delayed(Duration(milliseconds: 100));
    if (url.isEmpty) {
      throw Exception('URL cannot be empty');
    }
    return 'Data from $url';
  }

  Future<int> calculateAsync(int a, int b) async {
    await Future.delayed(Duration(milliseconds: 50));
    return a + b;
  }

  Stream<int> countStream(int start, int end) async* {
    for (int i = start; i <= end; i++) {
      await Future.delayed(Duration(milliseconds: 10));
      yield i;
    }
  }
}

// async_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'async_service.dart';

void main() {
  group('AsyncService Tests', () {
    late AsyncService service;

    setUp(() {
      service = AsyncService();
    });

    test('fetchData should return correct data', () async {
      // Given
      const url = 'https://api.example.com';

      // When
      final result = await service.fetchData(url);

      // Then
      expect(result, equals('Data from https://api.example.com'));
    });

    test('fetchData should throw exception for empty URL', () async {
      // Given
      const url = '';

      // When & Then
      await expectLater(
        () => service.fetchData(url),
        throwsA(const TypeMatcher<Exception>()),
      );
    });

    test('calculateAsync should return correct sum', () async {
      // Given
      const a = 5;
      const b = 3;

      // When
      final result = await service.calculateAsync(a, b);

      // Then
      expect(result, equals(8));
    });

    test('countStream should emit correct sequence', () async {
      // Given
      const start = 1;
      const end = 3;

      // When
      final stream = service.countStream(start, end);

      // Then
      await expectLater(
        stream,
        emitsInOrder(<int>[1, 2, 3]),
      );
    });

    test('countStream should emit nothing for invalid range', () async {
      // Given
      const start = 5;
      const end = 3;

      // When
      final stream = service.countStream(start, end);

      // Then
      await expectLater(
        stream,
        emitsDone,
      );
    });
  });
}

Mock测试

使用mockito库进行依赖模拟测试:

dart
// user_repository.dart
abstract class UserRepository {
  Future<User> getUser(int id);
  Future<List<User>> getAllUsers();
  Future<void> createUser(User user);
  Future<void> updateUser(User user);
  Future<void> deleteUser(int id);
}

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'email': email,
      };

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'],
        name: json['name'],
        email: json['email'],
      );
}

// user_service.dart
class UserService {
  final UserRepository _userRepository;

  UserService(this._userRepository);

  Future<User?> findUser(int id) async {
    try {
      return await _userRepository.getUser(id);
    } catch (e) {
      // 记录错误日志
      print('Error fetching user: $e');
      return null;
    }
  }

  Future<List<User>> getAllUsers() async {
    return await _userRepository.getAllUsers();
  }

  Future<bool> addUser(User user) async {
    try {
      await _userRepository.createUser(user);
      return true;
    } catch (e) {
      print('Error adding user: $e');
      return false;
    }
  }
}

// user_service_test.mocks.dart (Generated by build_runner)
// 注意:这部分通常是通过代码生成的
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
import 'user_repository.dart';
import 'user_service.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('UserService Tests', () {
    late UserService userService;
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
      userService = UserService(mockRepository);
    });

    test('findUser should return user when repository succeeds', () async {
      // Given
      const userId = 1;
      final mockUser = User(id: userId, name: 'John Doe', email: 'john@example.com');
      when(mockRepository.getUser(userId)).thenAnswer((_) async => mockUser);

      // When
      final result = await userService.findUser(userId);

      // Then
      expect(result, equals(mockUser));
      verify(mockRepository.getUser(userId)).called(1);
    });

    test('findUser should return null when repository throws', () async {
      // Given
      const userId = 1;
      when(mockRepository.getUser(userId))
          .thenThrow(Exception('User not found'));

      // When
      final result = await userService.findUser(userId);

      // Then
      expect(result, isNull);
      verify(mockRepository.getUser(userId)).called(1);
    });

    test('addUser should return true when repository succeeds', () async {
      // Given
      final user = User(id: 1, name: 'Jane Doe', email: 'jane@example.com');
      when(mockRepository.createUser(user)).thenAnswer((_) async => Future.value());

      // When
      final result = await userService.addUser(user);

      // Then
      expect(result, isTrue);
      verify(mockRepository.createUser(user)).called(1);
    });

    test('addUser should return false when repository throws', () async {
      // Given
      final user = User(id: 1, name: 'Jane Doe', email: 'jane@example.com');
      when(mockRepository.createUser(user))
          .thenThrow(Exception('Failed to create user'));

      // When
      final result = await userService.addUser(user);

      // Then
      expect(result, isFalse);
      verify(mockRepository.createUser(user)).called(1);
    });
  });
}

小部件测试

小部件测试用于测试Flutter小部件的UI表现和交互行为。

基础小部件测试

dart
// counter_widget.dart
import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    if (_counter > 0) {
      setState(() {
        _counter--;
      });
    }
  }

  void _resetCounter() {
    setState(() {
      _counter = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Count:',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.displayLarge,
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton(
                  onPressed: _decrementCounter,
                  child: Icon(Icons.remove),
                  heroTag: 'decrement',
                ),
                SizedBox(width: 20),
                FloatingActionButton(
                  onPressed: _incrementCounter,
                  child: Icon(Icons.add),
                  heroTag: 'increment',
                ),
              ],
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _resetCounter,
              child: Text('Reset'),
            ),
          ],
        ),
      ),
    );
  }
}

// counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'counter_widget.dart';

void main() {
  group('CounterWidget Tests', () {
    testWidgets('should start with count 0', (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // Then
      expect(find.text('0'), findsOneWidget);
      expect(find.text('Current Count:'), findsOneWidget);
    });

    testWidgets('should increment counter when "+" button is pressed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // When
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump(); // Rebuild after state change

      // Then
      expect(find.text('1'), findsOneWidget);
    });

    testWidgets('should decrement counter when "-" button is pressed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // When
      await tester.tap(find.byIcon(Icons.add)); // First increment to 1
      await tester.pump();
      await tester.tap(find.byIcon(Icons.remove)); // Then decrement to 0
      await tester.pump();

      // Then
      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('should not go below 0 when "-" button is pressed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // When
      await tester.tap(find.byIcon(Icons.remove)); // Try to decrement from 0
      await tester.pump();

      // Then
      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('should reset counter when reset button is pressed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // When - increment to some value
      await tester.tap(find.byIcon(Icons.add));
      await tester.tap(find.byIcon(Icons.add));
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();
      
      // Verify it's not 0
      expect(find.text('3'), findsOneWidget);

      // Reset
      await tester.tap(find.text('Reset'));
      await tester.pump();

      // Then
      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('should find all expected widgets', (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // Then
      expect(find.byType(Scaffold), findsOneWidget);
      expect(find.byType(AppBar), findsOneWidget);
      expect(find.byType(FloatingActionButton), findsNWidgets(2));
      expect(find.byType(ElevatedButton), findsOneWidget);
      expect(find.byIcon(Icons.add), findsOneWidget);
      expect(find.byIcon(Icons.remove), findsOneWidget);
    });
  });
}

小部件交互测试

dart
// form_widget.dart
import 'package:flutter/material.dart';

class FormWidget extends StatefulWidget {
  @override
  _FormWidgetState createState() => _FormWidgetState();
}

class _FormWidgetState extends State<FormWidget> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  
  String _submittedData = '';

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      setState(() {
        _submittedData = 'Name: ${_nameController.text}\n'
                         'Email: ${_emailController.text}\n'
                         'Password: ${_passwordController.text}';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Form Validation')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _nameController,
                decoration: InputDecoration(
                  labelText: 'Name',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Name is required';
                  }
                  if (value.length < 3) {
                    return 'Name must be at least 3 characters';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Email is required';
                  }
                  if (!value.contains('@')) {
                    return 'Invalid email format';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password is required';
                  }
                  if (value.length < 6) {
                    return 'Password must be at least 6 characters';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: _submitForm,
                child: Text('Submit'),
              ),
              SizedBox(height: 16),
              if (_submittedData.isNotEmpty)
                Card(
                  child: Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Text(_submittedData),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

// form_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'form_widget.dart';

void main() {
  group('FormWidget Tests', () {
    testWidgets('should show validation errors when fields are empty', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: FormWidget()));

      // When - tap submit without filling fields
      await tester.tap(find.text('Submit'));
      await tester.pump();

      // Then - expect validation errors
      expect(find.text('Name is required'), findsOneWidget);
      expect(find.text('Email is required'), findsOneWidget);
      expect(find.text('Password is required'), findsOneWidget);
    });

    testWidgets('should show specific validation errors', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: FormWidget()));

      // When - enter invalid data
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Name',
      ), 'Jo'); // Too short name
      
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Email',
      ), 'invalid-email'); // Invalid email
      
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Password',
      ), '123'); // Too short password

      // Submit
      await tester.tap(find.text('Submit'));
      await tester.pump();

      // Then - expect specific validation errors
      expect(find.text('Name must be at least 3 characters'), findsOneWidget);
      expect(find.text('Invalid email format'), findsOneWidget);
      expect(find.text('Password must be at least 6 characters'), findsOneWidget);
    });

    testWidgets('should submit successfully with valid data', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: FormWidget()));

      // When - enter valid data
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Name',
      ), 'John Doe');
      
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Email',
      ), 'john@example.com');
      
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Password',
      ), 'password123');

      // Submit
      await tester.tap(find.text('Submit'));
      await tester.pump();

      // Then - expect successful submission
      expect(find.text('Name: John Doe'), findsOneWidget);
      expect(find.text('Email: john@example.com'), findsOneWidget);
      expect(find.text('Password: password123'), findsOneWidget);
    });

    testWidgets('should update text fields correctly', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: FormWidget()));

      // When - enter text into fields
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Name',
      ), 'Test User');
      
      await tester.enterText(find.byWidgetPredicate(
        (widget) => widget is TextFormField && 
                   widget.decoration?.labelText == 'Email',
      ), 'test@example.com');

      // Then - expect text to be entered
      expect(find.text('Test User'), findsOneWidget);
      expect(find.text('test@example.com'), findsOneWidget);
    });
  });
}

小部件状态管理测试

dart
// state_management_widget.dart
import 'package:flutter/material.dart';

class TodoItem {
  final String id;
  final String title;
  bool isCompleted;

  TodoItem({
    required this.id,
    required this.title,
    this.isCompleted = false,
  });
}

class TodoListWidget extends StatefulWidget {
  @override
  _TodoListWidgetState createState() => _TodoListWidgetState();
}

class _TodoListWidgetState extends State<TodoListWidget> {
  final List<TodoItem> _todos = [
    TodoItem(id: '1', title: 'Task 1'),
    TodoItem(id: '2', title: 'Task 2'),
    TodoItem(id: '3', title: 'Task 3'),
  ];

  void _addTodo(String title) {
    setState(() {
      _todos.add(TodoItem(id: DateTime.now().millisecondsSinceEpoch.toString(), title: title));
    });
  }

  void _toggleTodo(String id) {
    setState(() {
      final todo = _todos.firstWhere((todo) => todo.id == id);
      todo.isCompleted = !todo.isCompleted;
    });
  }

  void _removeTodo(String id) {
    setState(() {
      _todos.removeWhere((todo) => todo.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Todo List')),
      body: ListView.builder(
        itemCount: _todos.length,
        itemBuilder: (context, index) {
          final todo = _todos[index];
          return Dismissible(
            key: Key(todo.id),
            onDismissed: (_) {
              _removeTodo(todo.id);
            },
            child: CheckboxListTile(
              title: Text(todo.title),
              value: todo.isCompleted,
              onChanged: (bool? value) {
                if (value != null) {
                  _toggleTodo(todo.id);
                }
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _addTodo('New Task ${_todos.length + 1}');
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

// state_management_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'state_management_widget.dart';

void main() {
  group('TodoListWidget Tests', () {
    testWidgets('should display initial todos', (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: TodoListWidget()));

      // Then
      expect(find.text('Task 1'), findsOneWidget);
      expect(find.text('Task 2'), findsOneWidget);
      expect(find.text('Task 3'), findsOneWidget);
      expect(find.byType(CheckboxListTile), findsNWidgets(3));
    });

    testWidgets('should add new todo when FAB is pressed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: TodoListWidget()));
      
      // Record initial count
      final initialCount = tester.widgetList<CheckboxListTile>(find.byType(CheckboxListTile)).length;

      // When
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();

      // Then
      final newCount = tester.widgetList<CheckboxListTile>(find.byType(CheckboxListTile)).length;
      expect(newCount, equals(initialCount + 1));
      expect(find.text('New Task 4'), findsOneWidget);
    });

    testWidgets('should toggle todo completion status', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: TodoListWidget()));

      // When - tap the checkbox for first todo
      await tester.tap(find.byType(Checkbox).first);
      await tester.pump();

      // Then - verify it's checked
      expect(find.byWidgetPredicate(
        (widget) => widget is Checkbox && widget.value == true,
      ), findsOneWidget);
    });

    testWidgets('should remove todo when dismissed', 
        (WidgetTester tester) async {
      // Given
      await tester.pumpWidget(MaterialApp(home: TodoListWidget()));
      
      final initialCount = tester.widgetList<CheckboxListTile>(find.byType(CheckboxListTile)).length;

      // When - swipe to dismiss first item
      await tester.drag(find.byType(Dismissible).first, Offset(-500, 0));
      await tester.pumpAndSettle(); // Allow dismissal animation to complete

      // Then
      final newCount = tester.widgetList<CheckboxListTile>(find.byType(CheckboxListTile)).length;
      expect(newCount, equals(initialCount - 1));
    });
  });
}

集成测试

集成测试用于测试多个组件协同工作的功能。

基础集成测试

dart
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('App Integration Tests', () {
    testWidgets('complete app workflow', (WidgetTester tester) async {
      // Given - start the app
      app.main();
      await tester.pumpAndSettle();

      // Then - verify initial state
      expect(find.text('Welcome'), findsOneWidget);
      expect(find.byType(TextField), findsOneWidget);
      expect(find.byType(ElevatedButton), findsOneWidget);

      // When - enter text and submit
      await tester.enterText(find.byType(TextField), 'Test Input');
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      // Then - verify state change
      expect(find.text('Test Input'), findsOneWidget);
      expect(find.text('Processed: Test Input'), findsOneWidget);

      // When - navigate to another screen
      await tester.tap(find.text('Go to Details'));
      await tester.pumpAndSettle();

      // Then - verify navigation
      expect(find.text('Details Screen'), findsOneWidget);
      expect(find.text('Back'), findsOneWidget);

      // When - go back
      await tester.tap(find.text('Back'));
      await tester.pumpAndSettle();

      // Then - verify back navigation
      expect(find.text('Welcome'), findsOneWidget);
    });

    testWidgets('form validation workflow', (WidgetTester tester) async {
      // Given
      app.main();
      await tester.pumpAndSettle();

      // When - submit empty form
      await tester.tap(find.text('Submit'));
      await tester.pump();

      // Then - expect validation errors
      expect(find.text('Field is required'), findsOneWidget);

      // When - fill valid data and submit
      await tester.enterText(find.byType(TextField), 'Valid Input');
      await tester.tap(find.text('Submit'));
      await tester.pump();

      // Then - expect success
      expect(find.text('Success'), findsOneWidget);
    });
  });
}

网络集成测试

dart
// network_integration_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

// Mock生成注解
@GenerateMocks([http.Client])
import 'network_integration_test.mocks.dart';

class ApiService {
  final http.Client client;

  ApiService(this.client);

  Future<String> getData(String url) async {
    final response = await client.get(Uri.parse(url));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data');
    }
  }
}

class DataWidget extends StatefulWidget {
  final ApiService apiService;

  const DataWidget({Key? key, required this.apiService}) : super(key: key);

  @override
  _DataWidgetState createState() => _DataWidgetState();
}

class _DataWidgetState extends State<DataWidget> {
  String _data = '';
  bool _loading = false;
  String _error = '';

  Future<void> _fetchData() async {
    setState(() {
      _loading = true;
      _error = '';
    });

    try {
      final data = await widget.apiService.getData('https://api.example.com/data');
      setState(() {
        _data = data;
        _loading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Data Fetching')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _fetchData,
              child: Text('Fetch Data'),
            ),
            SizedBox(height: 20),
            if (_loading)
              CircularProgressIndicator()
            else if (_error.isNotEmpty)
              Text('Error: $_error', style: TextStyle(color: Colors.red))
            else if (_data.isNotEmpty)
              Text(_data),
          ],
        ),
      ),
    );
  }
}

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Network Integration Tests', () {
    late MockClient mockClient;
    late ApiService apiService;

    setUp(() {
      mockClient = MockClient();
      apiService = ApiService(mockClient);
    });

    testWidgets('should display data when API call succeeds', 
        (WidgetTester tester) async {
      // Given
      const testData = '{"message": "Hello World"}';
      when(mockClient.get(Uri.parse('https://api.example.com/data')))
          .thenAnswer((_) async => http.Response(testData, 200));

      await tester.pumpWidget(
        MaterialApp(
          home: DataWidget(apiService: apiService),
        ),
      );

      // When
      await tester.tap(find.text('Fetch Data'));
      await tester.pump(); // First pump for loading state
      await tester.pump(Duration(milliseconds: 100)); // Wait for async call

      // Then
      verify(mockClient.get(Uri.parse('https://api.example.com/data'))).called(1);
      expect(find.text(testData), findsOneWidget);
    });

    testWidgets('should display error when API call fails', 
        (WidgetTester tester) async {
      // Given
      when(mockClient.get(Uri.parse('https://api.example.com/data')))
          .thenThrow(Exception('Network error'));

      await tester.pumpWidget(
        MaterialApp(
          home: DataWidget(apiService: apiService),
        ),
      );

      // When
      await tester.tap(find.text('Fetch Data'));
      await tester.pump(); // First pump for loading state
      await tester.pump(Duration(milliseconds: 100)); // Wait for async call

      // Then
      expect(find.textContaining('Exception: Network error'), findsOneWidget);
    });
  });
}

测试最佳实践

1. 测试结构组织

dart
// well_structured_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('FeatureUnderTest', () {
    // Setup code that runs before each test
    setUp(() {
      // Initialize dependencies, mocks, etc.
    });

    tearDown(() {
      // Clean up after each test
    });

    group('when condition A', () {
      setUp(() {
        // Specific setup for condition A
      });

      test('should do X', () {
        // Given
        // When
        // Then
      });

      test('should not do Y', () {
        // Given
        // When
        // Then
      });
    });

    group('when condition B', () {
      setUp(() {
        // Specific setup for condition B
      });

      test('should do Z', () {
        // Given
        // When
        // Then
      });
    });
  });
}

2. 测试数据生成

dart
// test_data_generator.dart
class TestDataGenerator {
  static User createUser({
    int? id,
    String? name,
    String? email,
  }) {
    return User(
      id: id ?? DateTime.now().millisecondsSinceEpoch,
      name: name ?? 'Test User ${DateTime.now().millisecond}',
      email: email ?? 'test${DateTime.now().millisecond}@example.com',
    );
  }

  static List<User> createUsers(int count) {
    return List.generate(count, (index) => createUser(
      id: index,
      name: 'User $index',
      email: 'user$index@example.com',
    ));
  }

  static Map<String, dynamic> createApiResponse({
    bool success = true,
    String? message,
    dynamic data,
  }) {
    return {
      'success': success,
      'message': message ?? (success ? 'Success' : 'Error'),
      'data': data,
    };
  }
}

3. 异步测试工具

dart
// async_test_utils.dart
import 'package:fake_async/fake_async.dart';

class AsyncTestUtils {
  static void testWithFakeAsync(Function(FakeAsync) testFn) {
    fakeAsync((async) {
      testFn(async);
      async.flushMicrotasks();
    });
  }

  static Future<void> waitForDuration(Duration duration) {
    return Future.delayed(duration);
  }

  static Future<void> waitForCondition(
    bool Function() condition, {
    Duration timeout = const Duration(seconds: 5),
    Duration checkInterval = const Duration(milliseconds: 100),
  }) async {
    final stopwatch = Stopwatch()..start();
    
    while (!condition() && stopwatch.elapsed < timeout) {
      await Future.delayed(checkInterval);
    }
    
    if (stopwatch.elapsed >= timeout) {
      throw TimeoutException('Condition not met within timeout', timeout);
    }
  }
}

4. 测试覆盖率

bash
# 运行测试并生成覆盖率报告
flutter test --coverage

# 生成HTML格式的覆盖率报告
genhtml coverage/lcov.info -o coverage/html

# 查看覆盖率摘要
lcov --summary coverage/lcov.info

5. 测试配置

yaml
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml

analyzer:
  exclude:
    - "**/*.g.dart"
    - "**/*.freezed.dart"
    - "lib/**/*.mocks.dart"
  strong-mode:
    implicit-dynamic: false

linter:
  rules:
    - prefer_const_constructors
    - prefer_const_literals_to_create_immutables
    - avoid_print
    - prefer_single_quotes

6. CI/CD测试配置

yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.x'
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test --coverage

      - name: Check formatting
        run: flutter format --set-exit-if-changed .

      - name: Analyze code
        run: flutter analyze

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

Mock工具使用

使用Mockito进行高级模拟

dart
// advanced_mocking_test.dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';

// 假设的服务类
abstract class PaymentService {
  Future<bool> processPayment(double amount, String currency);
  Future<String> getExchangeRate(String fromCurrency, String toCurrency);
}

// 生成Mock的注解
@GenerateMocks([PaymentService])
import 'advanced_mocking_test.mocks.dart';

void main() {
  group('Advanced Mocking Tests', () {
    late MockPaymentService mockPaymentService;

    setUp(() {
      mockPaymentService = MockPaymentService();
    });

    test('should handle different scenarios with argument matchers', () async {
      // 给定 - 设置不同的返回值基于参数
      when(mockPaymentService.processPayment(
        any,
        argThat(contains('USD'), named: 'currency'),
      )).thenAnswer((_) async => true);

      when(mockPaymentService.processPayment(
        argThat(greaterThan(1000)),
        anyNamed('currency'),
      )).thenThrow(Exception('Amount too high'));

      // 当 - 调用服务
      final result1 = await mockPaymentService.processPayment(500, 'USD');
      bool result2 = false;
      try {
        await mockPaymentService.processPayment(1500, 'EUR');
      } catch (e) {
        result2 = true; // Expected exception
      }

      // 那么 - 验证结果
      expect(result1, isTrue);
      expect(result2, isTrue);
      
      // 验证调用次数和参数
      verify(mockPaymentService.processPayment(500, 'USD')).called(1);
      verify(mockPaymentService.processPayment(1500, 'EUR')).called(1);
    });

    test('should verify sequential calls', () async {
      // 给定
      when(mockPaymentService.getExchangeRate(any, any))
          .thenAnswer((_) async => '1.2');

      // 当
      final rate1 = await mockPaymentService.getExchangeRate('USD', 'EUR');
      final rate2 = await mockPaymentService.getExchangeRate('EUR', 'GBP');

      // 那么
      verifyInOrder([
        mockPaymentService.getExchangeRate('USD', 'EUR'),
        mockPaymentService.getExchangeRate('EUR', 'GBP'),
      ]);
      verifyNoMoreInteractions(mockPaymentService);
    });
  });
}

性能测试

dart
// performance_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('measure widget build performance', (WidgetTester tester) async {
    final stopwatch = Stopwatch()..start();

    await tester.pumpWidget(
      MaterialApp(
        home: ListView.builder(
          itemCount: 1000,
          itemBuilder: (context, index) => ListTile(
            title: Text('Item $index'),
          ),
        ),
      ),
    );

    stopwatch.stop();
    print('Build time: ${stopwatch.elapsedMicroseconds} μs');
    
    // 断言构建时间在可接受范围内(例如小于100ms)
    expect(stopwatch.elapsedMilliseconds, lessThan(100));
  });

  test('measure algorithm performance', () {
    final stopwatch = Stopwatch()..start();
    
    // 测试算法性能
    final result = List.generate(10000, (index) => index * 2)
        .where((element) => element.isEven)
        .toList();
    
    stopwatch.stop();
    print('Algorithm time: ${stopwatch.elapsedMicroseconds} μs');
    
    expect(stopwatch.elapsedMilliseconds, lessThan(50));
    expect(result.length, equals(10000)); // All numbers are even after *2
  });
}

测试工具和技巧

Golden文件测试

dart
// golden_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('CounterWidget should match golden file', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Text(
              'Golden Test',
              style: TextStyle(fontSize: 24),
            ),
          ),
        ),
      ),
    );

    await expectLater(
      find.byType(Scaffold),
      matchesGoldenFile('goldens/snapshot.png'),
    );
  });
}

测试驱动开发(TDD)示例

dart
// tdd_example_test.dart (First write the test)
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Calculator TDD', () {
    test('multiply should return product of two numbers', () {
      // First write failing test
      final calculator = Calculator(); // This class doesn't exist yet
      final result = calculator.multiply(3, 4); // This method doesn't exist yet
      expect(result, equals(12));
    });
  });
}

// Then create the implementation
class Calculator {
  int multiply(int a, int b) {
    return a * b;
  }
}

总结

Flutter测试策略包括:

  1. 单元测试:测试独立的功能单元
  2. 小部件测试:测试UI组件的表现和交互
  3. 集成测试:测试多组件协作
  4. 端到端测试:测试完整应用流程

测试最佳实践:

  • 使用Given-When-Then模式编写测试
  • 合理使用Mock进行依赖隔离
  • 保持测试独立和可重复
  • 使用有意义的测试名称
  • 测试边界条件和异常情况
  • 维护适当的测试覆盖率

通过全面的测试策略,可以确保Flutter应用的质量、稳定性和可维护性。