dart-generate-test-mocks

dart-generate-test-mocks

热门

使用 `package:mockito` 和 `build_runner` 为外部依赖定义并生成 Mock 对象。适用于对依赖 API、数据库等复杂外部服务的类进行单元测试的场景。

2792Star
164Fork
更新于 2026/8/5
SKILL.md
只读
名称
dart-generate-test-mocks
描述

使用 `package:mockito` 和 `build_runner` 为外部依赖定义并生成 Mock 对象。适用于对依赖 API、数据库等复杂外部服务的类进行单元测试的场景。

Dart 应用的测试与 Mock 模拟

目录

针对可测试性构建代码结构

设计 Dart 类时应支持依赖注入(Dependency Injection)。将复杂的外部依赖(如 API 客户端或数据库)隔离开来,以便在测试期间替换为 Mock 对象。

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

管理依赖

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;

生成 Mock 对象

使用 package:mockitobuild_runner 自动生成 Mock 类,以便进行固定场景模拟与行为验证。

  • 始终推荐使用 @GenerateNiceMocks 注解(优先于 @GenerateMocks,以避免出现未存根/未 Stub 调用的抛错)。
  • 在测试文件中添加该注解,并传入 MockSpec<Type>() 对象列表。
  • 引入后缀为 .mocks.dart 的生成文件。
  • 执行 build_runner 命令生成 Mock 文件:dart run build_runner build

编写单元测试

使用生成的 Mock 对象隔离被测系统(System Under Test)。使用 package:test 构建测试套件。

  • 存根配置(Stubbing): 在与被测系统交互前配置 Mock 对象的行为。
    • 对于同步方法,使用 when(mock.method()).thenReturn(value)
    • 关键: 返回 FutureStream 的异步方法必须使用 thenAnswer((_) async => value)。切勿对异步返回使用 thenReturn
  • 行为验证(Verification): 断言被测系统是否与 Mock 对象进行了正确的交互。
    • 使用 verify(mock.method()).called(1) 检查精确的调用次数。
    • 使用参数匹配器(如 anyanyNamedcaptureAny)进行灵活校验。

工作流:创建并运行 Mock 测试

使用以下检查清单来实现和验证带有 Mock 的单元测试。

任务进度

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

关联反馈:测试失败排查

如果测试失败或 build_runner 报错:

  1. 运行校验: 执行 dart testdart run build_runner build
  2. 排查错误: 检查是否存在缺失的 Stub、不匹配的参数匹配器或生成文件中的语法错误。
  3. 修复:
    • 如果 Mock 方法抛出意料之外的 null 错误,确保使用的是 @GenerateNiceMocks
    • 如果异步 Stub 抛出 ArgumentError,请将 thenReturn 改为 thenAnswer
    • 如果 build_runner 运行失败,确保 .mocks.dart 的导入路径与文件名完全匹配。
  4. 重复上述步骤,直到所有测试通过。

完整示例

高保真 Mock 与测试示例

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 生成 Mock 类
@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('returns data if the http call completes successfully', () async {
      // Arrange: 使用 thenAnswer 配置异步 HTTP GET 请求的 Stub
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('{"data": "Success"}', 200),
      );

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

      // Assert
      expect(result, 'Success');

      // 验证 Mock 是否传入正确的 Uri 并被调用了 1 次
      verify(mockHttpClient.get(Uri.parse('https://api.example.com/data'))).called(1);
    });

    test('throws an exception if the http call completes with an error', () {
      // Arrange
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('Not Found', 404),
      );

      // Act & Assert
      expect(
        apiService.fetchData('https://api.example.com/data'),
        throwsException,
      );
    });
  });
}