关于 dart-generate-test-mocks
面向 Dart 开发者的测试指南,涵盖依赖注入、Mockito 模拟对象生成和单元测试流程,并着重说明异步存根与交互验证。
主要功能
- 01通过依赖注入提升可测试性
- 02使用 Mockito 和 build_runner 生成模拟对象
- 03为同步及异步方法设置存根
- 04验证模拟对象交互与参数
适用场景
- 01在单元测试中模拟 HTTP 客户端
- 02为外部服务生成测试替身
- 03排查模拟测试失败
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dart-lang/ skills --skill dart-generate-test-mocks安装后在 Claude Code 中输入/dart-generate-test-mocks确认已加载。
详细介绍
测试和模拟 Dart 应用程序
目录
设计便于测试的代码结构
设计 Dart 类时应支持依赖注入。隔离复杂的外部依赖(如 API 客户端或数据库),以便在测试期间用模拟对象替换它们。
- 通过类构造函数注入外部服务(例如,
http.Client)。 - 使用
Uri.parse(string)严格将 URL 表示为Uri对象。 - 利用 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:mockito 和 build_runner,自动生成用于固定场景和行为验证的模拟类。
- 始终使用
@GenerateNiceMocks注解(优先于@GenerateMocks,以避免出现缺少存根异常)。 - 将注解放在测试文件中,并传入一个
MockSpec<Type>()对象列表。 - 使用
.mocks.dart扩展名导入生成的文件。 - 执行
build_runner以生成模拟文件:dart run build_runner build。
实现单元测试
使用生成的模拟对象隔离被测系统。使用 package:test 组织测试套件。
- 设置存根: 在与被测系统交互之前配置模拟对象的行为。
- 对于同步方法,使用
when(mock.method()).thenReturn(value)。 - 关键: 对于返回
Future或Stream的方法,始终使用thenAnswer((_) async => value)。异步返回值切勿使用thenReturn。
- 对于同步方法,使用
- 验证: 断言被测系统是否与模拟对象正确交互。
- 使用
verify(mock.method()).called(1)检查精确的调用次数。 - 使用
any、anyNamed或captureAny等参数匹配器进行灵活验证。
- 使用
工作流:创建和运行模拟测试
使用以下清单来实现和验证模拟单元测试。
任务进度
- 1. 确定要模拟的外部依赖(例如,
http.Client)。 - 2. 将该依赖注入目标类的构造函数。
- 3. 创建测试文件(例如,
target_test.dart),并添加@GenerateNiceMocks([MockSpec<Dependency>()])。 - 4. 为生成的
.mocks.dart文件添加part或import指令。 - 5. 运行
dart run build_runner build以生成模拟类。 - 6. 使用
group()和test()编写测试用例。 - 7. 使用
when()为所需行为设置存根。 - 8. 执行目标方法。
- 9. 使用
verify()验证交互,并使用expect()断言结果。 - 10. 使用
dart test运行测试套件。
反馈循环:测试失败
如果测试失败,或 build_runner 遇到错误:
- 运行验证器: 执行
dart test或dart run build_runner build。 - 检查错误: 检查是否缺少存根、参数匹配器不匹配,或生成的文件中存在语法错误。
- 修复:
- 如果模拟方法抛出意外的 null 错误,请确保使用了
@GenerateNiceMocks。 - 如果异步存根抛出
ArgumentError,请将thenReturn改为thenAnswer。 - 如果
build_runner失败,请确保.mocks.dart导入与文件名完全一致。
- 如果模拟方法抛出意外的 null 错误,请确保使用了
- 重复上述步骤,直到所有测试通过。
示例
高保真模拟与测试示例
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';
// Generate the mock class for 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('returns data if the http call completes successfully', () async {
// Arrange: Stub the async HTTP GET request using thenAnswer
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');
// Verify the mock was called with the correct Uri
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,
);
});
});
}
评论
支持 Markdown