dart-generate-test-mocks

dart-generate-test-mocks

热门

使用 `package:mockito` 和 `build_runner` 定义并生成外部依赖的模拟对象。适用于对依赖复杂外部服务(如 API 或数据库)的类进行单元测试。

392Star
25Fork
更新于 2026/7/10
SKILL.md
readonly只读
name
dart-generate-test-mocks
description

使用 `package:mockito` 和 `build_runner` 定义并生成外部依赖的模拟对象。适用于对依赖复杂外部服务(如 API 或数据库)的类进行单元测试。

测试与模拟 Dart 应用

目录

为可测试性组织代码

设计 Dart 类以支持依赖注入。将复杂的外部依赖(如 API 客户端或数据库)隔离,以便在测试时用模拟对象替换。

  • 通过类构造函数注入外部服务(例如 http.Client)。
  • 严格使用 Uri 对象表示 URL,通过 Uri.parse(string) 创建。
  • 利用 Dart 的面向对象特性(类、混入)为外部交互定义清晰的接口。

管理依赖

pubspec.yaml 文件中配置必要的测试和代码生成包。

  • 使用 dart pub add http 添加运行时依赖(例如 package:http)。
  • 使用 dart pub add dev:test dev:mockito dev:build_runner 添加测试依赖。
  • 使用前缀导入 HTTP 库以避免命名空间冲突:import 'package:http/http.dart' as http;

生成模拟对象

使用 package:mockitobuild_runner 自动生成用于固定场景和行为验证的模拟类。

  • 始终使用 @GenerateNiceMocks 注解(优于 @GenerateMocks,以避免缺失桩异常)。
  • 将注解放在测试文件中,传入 MockSpec<Type>() 对象的列表。
  • 使用 .mocks.dart 扩展名导入生成的文件。
  • 执行 build_runner 生成模拟文件:dart run build_runner build

实现单元测试

使用生成的模拟对象隔离被测系统。使用 package:test 组织测试套件。

  • 桩设置: 在与被测系统交互之前配置模拟行为。
    • 对于同步方法,使用 when(mock.method()).thenReturn(value)
    • 关键: 对于返回 FutureStream 的方法,始终使用 thenAnswer((_) async => value)。切勿对异步返回使用 thenReturn
  • 验证: 断言被测系统与模拟对象的交互正确。
    • 使用 verify(mock.method()).called(1) 检查精确的调用次数。
    • 使用参数匹配器如 anyanyNamedcaptureAny 进行灵活验证。

工作流:创建并运行带模拟的测试

使用以下清单实现并验证带模拟的单元测试。

任务进度

  • [ ] 1. 确定要模拟的外部依赖(例如 http.Client)。
  • [ ] 2. 将依赖注入目标类的构造函数。
  • [ ] 3. 创建测试文件(例如 target_test.dart)并添加 @GenerateNiceMocks([MockSpec<Dependency>()])
  • [ ] 4. 为生成的 .mocks.dart 文件添加 partimport 指令。
  • [ ] 5. 运行 dart run build_runner build 生成模拟类。
  • [ ] 6. 使用 group()test() 编写测试用例。
  • [ ] 7. 使用 when() 设置所需行为。
  • [ ] 8. 执行目标方法。
  • [ ] 9. 使用 verify() 验证交互,使用 expect() 断言结果。
  • [ ] 10. 使用 dart test 运行测试套件。

反馈循环:测试失败

如果测试失败或 build_runner 遇到错误:

  1. 运行验证器: 执行 dart testdart run build_runner build
  2. 检查错误: 检查缺失的桩、参数匹配器不匹配或生成文件中的语法错误。
  3. 修复:
    • 如果模拟方法抛出意外的 null 错误,确保使用了 @GenerateNiceMocks
    • 如果异步桩抛出 ArgumentError,将 thenReturn 改为 thenAnswer
    • 如果 build_runner 失败,确保 .mocks.dart 导入与文件名完全匹配。
  4. 重复直到所有测试通过。

示例

高保真模拟与测试示例

1. 被测系统(lib/api_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  final http.Client client;

  ApiService(this.client);

  Future<String> fetchData(String urlString) async {
    final uri = Uri.parse(urlString);
    final response = await client.get(uri);

    if (response.statusCode == 200) {
      return jsonDecode(response.body)['data'];
    } else {
      throw Exception('Failed to load data');
    }
  }
}

2. 测试实现(test/api_service_test.dart

import 'package:test/test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
import 'package:my_app/api_service.dart';

// 为 http.Client 生成模拟类
@GenerateNiceMocks([MockSpec<http.Client>()])
import 'api_service_test.mocks.dart';

void main() {
  group('ApiService', () {
    late ApiService apiService;
    late MockClient mockHttpClient;

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

    test('如果 HTTP 调用成功完成,返回数据', () async {
      // 准备:使用 thenAnswer 设置异步 HTTP GET 请求的桩
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('{"data": "Success"}', 200),
      );

      // 执行
      final result = await apiService.fetchData('https://api.example.com/data');

      // 断言
      expect(result, 'Success');

      // 验证模拟对象被使用正确的 Uri 调用
      verify(mockHttpClient.get(Uri.parse('https://api.example.com/data'))).called(1);
    });

    test('如果 HTTP 调用完成但返回错误,抛出异常', () {
      // 准备
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('Not Found', 404),
      );

      // 执行与断言
      expect(
        apiService.fetchData('https://api.example.com/data'),
        throwsException,
      );
    });
  });
}