Skip to content

测试

测试是保证代码质量的重要手段。Flutter 提供了三种测试级别:单元测试、Widget 测试和集成测试。

测试分类

类型测试对象运行速度依赖
单元测试函数、方法、类毫秒级无 UI
Widget 测试单个 Widget秒级Flutter 框架
集成测试完整 App 流程分钟级真实环境

单元测试

测试纯逻辑代码,不涉及 UI。

基本写法

dart
// test/utils/validators_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/utils/validators.dart';

void main() {
  group('Validators', () {
    test('required 返回 null 当值非空', () {
      expect(Validators.required('hello'), isNull);
    });

    test('required 返回错误提示 当值为空', () {
      expect(Validators.required(''), '此项不能为空');
      expect(Validators.required(null), '此项不能为空');
    });

    test('email 返回 null 当格式正确', () {
      expect(Validators.email('test@example.com'), isNull);
    });

    test('email 返回错误提示 当格式错误', () {
      expect(Validators.email('invalid'), '邮箱格式不正确');
    });
  });
}

测试 Model

dart
// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';

void main() {
  group('User', () {
    test('fromJson 正确解析', () {
      final json = {'name': '张三', 'age': 25};
      final user = User.fromJson(json);

      expect(user.name, '张三');
      expect(user.age, 25);
    });

    test('toJson 正确序列化', () {
      final user = User(name: '张三', age: 25);
      final json = user.toJson();

      expect(json['name'], '张三');
      expect(json['age'], 25);
    });
  });
}

运行测试

bash
# 运行所有测试
flutter test

# 运行指定文件
flutter test test/utils/validators_test.dart

# 运行匹配名称的测试
flutter test --name "email"

Widget 测试

测试单个 Widget 的渲染和交互。

基本写法

dart
// test/widgets/counter_display_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_display.dart';

void main() {
  testWidgets('CounterDisplay 显示正确的数字', (WidgetTester tester) async {
    // 1. 构建 Widget
    await tester.pumpWidget(
      MaterialApp(home: CounterDisplay(count: 42)),
    );

    // 2. 查找 Widget
    final textFinder = find.text('42');

    // 3. 验证
    expect(textFinder, findsOneWidget);
  });

  testWidgets('点击按钮触发回调', (WidgetTester tester) async {
    var pressed = false;

    await tester.pumpWidget(
      MaterialApp(
        home: ElevatedButton(
          onPressed: () => pressed = true,
          child: Text('点击'),
        ),
      ),
    );

    // 模拟点击
    await tester.tap(find.text('点击'));
    expect(pressed, isTrue);
  });
}

常用 Finder

Finder说明
find.text('hello')查找包含指定文字的 Widget
find.byType(Text)查找指定类型的 Widget
find.byKey(Key('myKey'))通过 Key 查找
find.byIcon(Icons.add)查找指定图标
find.widgetWithText(Card, '标题')查找包含文字的特定类型 Widget

常用 Matcher

Matcher说明
findsOneWidget找到恰好一个
findsNothing没找到
findsN(3)找到 N 个
findsWidgets找到至少一个

Mock 测试

使用 Mock 替换真实依赖(如网络请求),让测试更可控。

安装

bash
flutter pub add dev:mockito

使用

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

// 生成 Mock 类
@GenerateNiceMocks([MockSpec<ApiService>()])
import 'api_service_test.mocks.dart';

void main() {
  test('fetchUsers 返回用户列表', () async {
    // 创建 Mock
    final mockApi = MockApiService();
    when(mockApi.getUsers()).thenAnswer((_) async => [User(name: '张三', age: 25)]);

    // 使用 Mock
    final users = await mockApi.getUsers();

    expect(users.length, 1);
    expect(users[0].name, '张三');
  });
}

测试最佳实践

  1. 测试文件放在 test/ 目录下,与 lib/ 目录结构对应
  2. 文件名以 _test.dart 结尾,如 validators_test.dart
  3. 使用 group 组织相关测试
  4. 测试命名清晰,描述期望行为
  5. 每个测试只测一件事
  6. 异步测试使用 async/await
  7. 使用 Mock 隔离外部依赖

建议

  • 先学会写单元测试(最简单、最实用)
  • Widget 测试在关键组件上写即可
  • 集成测试在发布前跑一遍核心流程
  • 不要追求 100% 覆盖率,优先测试核心业务逻辑

下一步

基于 Flutter 官方文档整理