返回博客
测试2025年2月10日安安科技团队阅读时间 12 分钟

FLUTTER 测试
策略

全面讲解 Flutter 的 unit test、widget test 与 integration test。用可靠的测试策略,为你的代码建立十足信心。

Flutter 测试策略
// 简报

测试不只是一种好习惯——它是构建可靠 Flutter 应用的必要条件。完善的测试能节省时间、防止 bug 流入生产环境,更让你有信心重构代码、添加新功能,而不必担心破坏现有功能。

Flutter 开箱即用地提供了出色的测试工具。这份完整指南涵盖 unit test、widget test、integration test,以及成功的 Flutter 团队所采用的高级测试模式。你将学会编写易维护、运行快且可靠的测试。

UNIT TEST:测试你的逻辑

Unit test 用于独立验证单个函数、方法和类。它们运行速度快、容易编写,构成测试金字塔的基石。

calculator_test.dart
// Model class to test
class Calculator {
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
  double divide(int a, int b) {
    if (b == 0) throw ArgumentError('Cannot divide by zero');
    return a / b;
  }
}

// Unit tests
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/calculator.dart';

void main() {
  group('Calculator', () {
    late Calculator calculator;
    
    setUp(() {
      calculator = Calculator();
    });
    
    test('should add two numbers correctly', () {
      // Arrange
      const a = 5;
      const b = 3;
      
      // Act
      final result = calculator.add(a, b);
      
      // Assert
      expect(result, equals(8));
    });
    
    test('should subtract two numbers correctly', () {
      expect(calculator.subtract(10, 3), equals(7));
    });
    
    test('should divide two numbers correctly', () {
      expect(calculator.divide(10, 2), equals(5.0));
    });
    
    test('should throw error when dividing by zero', () {
      expect(
        () => calculator.divide(10, 0),
        throwsA(isA<ArgumentError>()),
      );
    });
  });
  
  group('Calculator Edge Cases', () {
    test('should handle negative numbers', () {
      final calculator = Calculator();
      expect(calculator.add(-5, 3), equals(-2));
      expect(calculator.subtract(-5, -3), equals(-2));
    });
    
    test('should handle large numbers', () {
      final calculator = Calculator();
      expect(calculator.add(999999, 1), equals(1000000));
    });
  });
}

Unit Test 最佳实践:

  • 遵循 AAA 模式:Arrange(准备)、Act(执行)、Assert(断言)
  • 使用能清楚描述行为的测试名称
  • 每次只测试一件事
  • 用 setUp() 和 tearDown() 处理公共初始化
  • 将相关测试分组管理

WIDGET TEST:测试你的 UI

Widget test 用于验证 UI 组件是否正常工作。它们在受控环境中测试用户交互、状态变化和 widget 行为。

widget_test.dart
// Widget to test
class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;
  
  void _increment() {
    setState(() {
      _counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count: $_counter', key: Key('counter-text')),
            ElevatedButton(
              key: Key('increment-button'),
              onPressed: _increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

// Widget tests
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/counter_widget.dart';

void main() {
  group('CounterWidget', () {
    testWidgets('should display initial counter value', (WidgetTester tester) async {
      // Arrange & Act
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));
      
      // Assert
      expect(find.text('Count: 0'), findsOneWidget);
      expect(find.text('Increment'), findsOneWidget);
    });
    
    testWidgets('should increment counter when button is pressed', (WidgetTester tester) async {
      // Arrange
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));
      
      // Act
      await tester.tap(find.byKey(Key('increment-button')));
      await tester.pump(); // Trigger rebuild
      
      // Assert
      expect(find.text('Count: 1'), findsOneWidget);
      expect(find.text('Count: 0'), findsNothing);
    });
    
    testWidgets('should increment multiple times', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));
      
      // Tap button 3 times
      for (int i = 0; i < 3; i++) {
        await tester.tap(find.byKey(Key('increment-button')));
        await tester.pump();
      }
      
      expect(find.text('Count: 3'), findsOneWidget);
    });
  });
  
  group('CounterWidget Accessibility', () {
    testWidgets('should be accessible', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));
      
      // Check semantic labels
      expect(tester.getSemantics(find.byKey(Key('counter-text'))), 
             matchesSemantics(label: 'Count: 0'));
    });
  });
}

Widget Test 技巧:

  • 使用 key 可靠地定位 widget
  • 状态变更后记得调用 pump()
  • 测试点击、滚动等用户交互
  • 验证 widget 的属性与行为
  • 测试无障碍功能

INTEGRATION TEST:端到端

Integration test 用于验证应用各部分能否正确协作。它们在设备或模拟器上测试完整的用户流程和真实应用行为。

integration_test/app_test.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 'package:your_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('App Integration Tests', () {
    testWidgets('complete user flow test', (WidgetTester tester) async {
      // Start the app
      app.main();
      await tester.pumpAndSettle();
      
      // Test login flow
      await tester.enterText(find.byKey(Key('email-field')), 'test@example.com');
      await tester.enterText(find.byKey(Key('password-field')), 'password123');
      await tester.tap(find.byKey(Key('login-button')));
      await tester.pumpAndSettle();
      
      // Verify successful login
      expect(find.text('Welcome Back!'), findsOneWidget);
      
      // Test navigation to profile
      await tester.tap(find.byKey(Key('profile-tab')));
      await tester.pumpAndSettle();
      
      // Verify profile screen
      expect(find.text('Profile'), findsOneWidget);
      expect(find.text('test@example.com'), findsOneWidget);
      
      // Test logout
      await tester.tap(find.byKey(Key('logout-button')));
      await tester.pumpAndSettle();
      
      // Verify back to login screen
      expect(find.byKey(Key('login-button')), findsOneWidget);
    });
    
    testWidgets('offline functionality test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // Simulate offline mode
      await tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
        const MethodChannel('connectivity_plus'),
        (MethodCall methodCall) async {
          if (methodCall.method == 'check') {
            return 'none'; // No connectivity
          }
          return null;
        },
      );
      
      // Test app behavior when offline
      await tester.tap(find.byKey(Key('refresh-button')));
      await tester.pumpAndSettle();
      
      expect(find.text('No internet connection'), findsOneWidget);
    });
  });
  
  group('Performance Tests', () {
    testWidgets('scroll performance test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // Navigate to list screen
      await tester.tap(find.byKey(Key('list-tab')));
      await tester.pumpAndSettle();
      
      // Measure scroll performance
      final Stopwatch stopwatch = Stopwatch()..start();
      
      await tester.fling(find.byType(ListView), const Offset(0, -500), 1000);
      await tester.pumpAndSettle();
      
      stopwatch.stop();
      
      // Assert reasonable performance (adjust threshold as needed)
      expect(stopwatch.elapsedMilliseconds, lessThan(1000));
    });
  });
}

Integration Test 最佳实践:

  • 对关键用户流程进行端到端测试
  • 用 pumpAndSettle() 处理异步操作
  • 尽可能在真机上测试
  • 合理地 mock 外部依赖
  • 测量并测试性能

高级测试模式

通过 mocking、golden test 和各种测试模式,让测试更易维护、更可靠,把测试水平提升到新高度。

user_repository_test.dart
// Mocking with Mockito
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([ApiService, DatabaseService])
import 'test_file.mocks.dart';

class UserRepository {
  final ApiService apiService;
  final DatabaseService dbService;
  
  UserRepository(this.apiService, this.dbService);
  
  Future<User> getUser(String id) async {
    try {
      final user = await apiService.fetchUser(id);
      await dbService.saveUser(user);
      return user;
    } catch (e) {
      return await dbService.getUser(id);
    }
  }
}

// Test with mocks
void main() {
  group('UserRepository', () {
    late MockApiService mockApiService;
    late MockDatabaseService mockDbService;
    late UserRepository userRepository;
    
    setUp(() {
      mockApiService = MockApiService();
      mockDbService = MockDatabaseService();
      userRepository = UserRepository(mockApiService, mockDbService);
    });
    
    test('should return user from API and save to database', () async {
      // Arrange
      const userId = '123';
      final expectedUser = User(id: userId, name: 'John Doe');
      
      when(mockApiService.fetchUser(userId))
          .thenAnswer((_) async => expectedUser);
      when(mockDbService.saveUser(expectedUser))
          .thenAnswer((_) async => {});
      
      // Act
      final result = await userRepository.getUser(userId);
      
      // Assert
      expect(result, equals(expectedUser));
      verify(mockApiService.fetchUser(userId)).called(1);
      verify(mockDbService.saveUser(expectedUser)).called(1);
    });
    
    test('should fallback to database when API fails', () async {
      // Arrange
      const userId = '123';
      final expectedUser = User(id: userId, name: 'John Doe');
      
      when(mockApiService.fetchUser(userId))
          .thenThrow(Exception('Network error'));
      when(mockDbService.getUser(userId))
          .thenAnswer((_) async => expectedUser);
      
      // Act
      final result = await userRepository.getUser(userId);
      
      // Assert
      expect(result, equals(expectedUser));
      verify(mockApiService.fetchUser(userId)).called(1);
      verify(mockDbService.getUser(userId)).called(1);
      verifyNever(mockDbService.saveUser(any));
    });
  });
}

// Golden Tests for UI consistency
testWidgets('login screen golden test', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));
  await expectLater(
    find.byType(LoginScreen),
    matchesGoldenFile('golden/login_screen.png'),
  );
});

// Custom matchers
Matcher hasErrorMessage(String message) {
  return predicate<Widget>((widget) {
    if (widget is Text) {
      return widget.data == message && 
             widget.style?.color == Colors.red;
    }
    return false;
  }, 'has error message: $message');
}

高级测试技巧:

  • 使用依赖注入,让测试更简单
  • Mock 外部服务和 API
  • 用 golden test 保证 UI 一致性
  • 为领域特定的断言编写自定义 matcher
  • 用 test data builder 构建复杂对象

测试金字塔

UNIT TESTS

占测试的 70%
  • 执行速度快
  • 组件相互隔离
  • 易于调试
  • 覆盖率高

WIDGET TESTS

占测试的 20%
  • UI 交互
  • 状态管理
  • Widget 行为
  • 速度适中

INTEGRATION TESTS

占测试的 10%
  • 端到端流程
  • 真实用户场景
  • 完整应用测试
  • 执行速度较慢
// 覆盖率

测试需要帮助?

我们的团队可以帮你为 Flutter 应用搭建全面的测试策略。从测试环境搭建到 CI/CD 集成,我们全程覆盖!

联系我们
// 信号放大

分享本指南