Flutter 测试完全指南

引言

测试是软件质量保障的关键环节。本文将深入探讨 Flutter 测试的各种类型和最佳实践。

基础概念回顾

测试类型

  • 单元测试: 测试单个函数或方法
  • Widget 测试: 测试单个 Widget
  • 集成测试: 测试多个组件的交互
  • 性能测试: 测试应用性能

测试工具

  • test: Dart 核心测试库
  • flutter_test: Flutter Widget 测试库
  • integration_test: 集成测试库

高级技巧一:单元测试

基础结构

import 'package:test/test.dart';

void main() {
  group('Calculator', () {
    test('adds two numbers', () {
      final calculator = Calculator();
      expect(calculator.add(2, 3), equals(5));
    });

    test('subtracts two numbers', () {
      final calculator = Calculator();
      expect(calculator.subtract(5, 3), equals(2));
    });
  });
}

异步测试

test('fetches data from API', () async {
  final service = DataService();
  final result = await service.fetchData();
  
  expect(result, isNotNull);
  expect(result.length, greaterThan(0));
});

Mock 测试

class MockApiService extends Mock implements ApiService {}

test('uses mock service', () {
  final mockService = MockApiService();
  when(mockService.fetchUser()).thenAnswer((_) async => User(id: '1', name: 'Test'));
  
  final repository = UserRepository(mockService);
  final user = await repository.getUser();
  
  expect(user.name, equals('Test'));
  verify(mockService.fetchUser()).called(1);
});

高级技巧二:Widget 测试

基础结构

import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());
    
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);
    
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

状态测试

testWidgets('shows loading state', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: const LoadingScreen()));
  
  expect(find.byType(CircularProgressIndicator), findsOneWidget);
  
  await tester.pump(const Duration(seconds: 1));
  
  expect(find.byType(CircularProgressIndicator), findsNothing);
  expect(find.text('Loaded!'), findsOneWidget);
});

交互测试

testWidgets('form validation', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: const LoginForm()));
  
  await tester.enterText(find.byType(TextField), 'test');
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();
  
  expect(find.text('Please enter email'), findsOneWidget);
});

高级技巧三:集成测试

基础结构

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full app test', (WidgetTester tester) async {
    app.main();
    await tester.pumpAndSettle();
    
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();
    
    expect(find.text('Welcome'), findsOneWidget);
  });
}

性能测试

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('performance test', (WidgetTester tester) async {
    app.main();
    await tester.pumpAndSettle();
    
    final stopwatch = Stopwatch()..start();
    
    await tester.fling(find.byType(ListView), const Offset(0, -300), 300);
    await tester.pumpAndSettle();
    
    stopwatch.stop();
    expect(stopwatch.elapsedMilliseconds, lessThan(16));
  });
}

实战案例:登录测试

void main() {
  testWidgets('login flow test', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginPage()));
    
    expect(find.byType(TextField), findsNWidgets(2));
    
    await tester.enterText(find.byType(TextField).first, 'test@example.com');
    await tester.enterText(find.byType(TextField).last, 'password123');
    
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();
    
    expect(find.text('Dashboard'), findsOneWidget);
  });
}

实战案例:列表测试

void main() {
  testWidgets('list item tap navigates', (WidgetTester tester) async {
    final items = List.generate(10, (i) => 'Item $i');
    
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(items[index]),
              onTap: () => Navigator.push(context, MaterialPageRoute(
                builder: (_) => DetailPage(item: items[index]),
              )),
            );
          },
        ),
      ),
    ));
    
    await tester.tap(find.text('Item 3'));
    await tester.pumpAndSettle();
    
    expect(find.text('Detail: Item 3'), findsOneWidget);
  });
}

实战案例:状态管理测试

void main() {
  testWidgets('counter state updates', (WidgetTester tester) async {
    await tester.pumpWidget(ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: const MaterialApp(home: CounterPage()),
    ));
    
    expect(find.text('Count: 0'), findsOneWidget);
    
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    expect(find.text('Count: 1'), findsOneWidget);
    
    await tester.tap(find.byIcon(Icons.remove));
    await tester.pump();
    
    expect(find.text('Count: 0'), findsOneWidget);
  });
}

常见问题与解决方案

Q1:Widget 测试找不到元素?

A:确保 Widget 已正确渲染:

await tester.pumpWidget(const MyWidget());
await tester.pump();

Q2:异步测试超时?

A:使用 pumpAndSettle:

await tester.pumpAndSettle(const Duration(seconds: 5));

Q3:如何测试导航?

A:使用 NavigatorObserver:

final navigatorObserver = MockNavigatorObserver();

await tester.pumpWidget(MaterialApp(
  home: const HomePage(),
  navigatorObservers: [navigatorObserver],
));

最佳实践

1. 测试命名规范

test('description of what the test does', () {
  // test code
});

2. 使用 group 组织测试

group('Feature A', () {
  test('test 1', () {});
  test('test 2', () {});
});

3. 保持测试独立

test('test 1', () {
  // 不依赖其他测试
});

test('test 2', () {
  // 独立运行
});

总结

Flutter 测试是保障应用质量的关键。通过本文的学习,你应该能够:

  1. 编写单元测试
  2. 编写 Widget 测试
  3. 编写集成测试
  4. 测试状态管理
  5. 测试导航流程
  6. 遵循测试最佳实践

掌握这些技巧,能够帮助你构建更加可靠和稳定的应用。

更多推荐