测试工具
概述
测试工具模块(@core/testing)提供测试环境下的依赖注入容器管理能力。基于 @core/di 的子容器机制,在不影响全局服务注册的前提下,实现测试用例级别的服务 Mock 与隔离。
核心概念
为什么需要测试工具
在 HCompass 框架中,ViewModel 通过 DI 容器解析 Repository 等服务。编写单元测试时,需要将真实服务替换为 Mock 实现,同时不能破坏全局容器中其他模块的注册。
子容器隔离原理
TestContainerHelper 利用 ServiceContainer 的父子容器链式查找机制:
全局容器 (父) 测试子容器
+-----------------------+ +-----------------------+
| HttpClient (真实) | <---- | AuthRepo (Mock) |
| AuthRepo (真实) | | |
| UserRepo (真实) | | |
+-----------------------+ +-----------------------+
resolve(AuthRepo) -> 子容器命中,返回 Mock
resolve(HttpClient) -> 子容器未命中,回退父容器,返回真实服务
resolve(UserRepo) -> 子容器未命中,回退父容器,返回真实服务setup():基于当前全局容器创建子容器,子容器自动继承父容器的所有服务registerMock():在子容器中注册 Mock 服务,优先级高于父容器的同名服务teardown():恢复原始全局容器引用,所有真实服务完好无损
核心 API
TestContainerHelper
测试容器辅助类,提供完整的测试生命周期管理:
typescript
import { TestContainerHelper } from "@core/testing";
const helper = new TestContainerHelper();setup
初始化测试环境,保存当前全局容器并创建子容器:
typescript
helper.setup();注意
重复调用 setup() 而未先调用 teardown() 会抛出异常,防止丢失原始容器引用。
registerMock
注册 Mock 服务到测试子容器:
typescript
helper.registerMock<IAuthRepository>(AUTH_REPOSITORY_KEY, mockAuthRepo);注意
必须在 setup() 之后调用,否则会抛出异常。
teardown
清理测试环境,恢复原始全局容器:
typescript
helper.teardown();使用示例
1. 添加依赖
在需要编写测试的功能包 oh-package.json5 中添加依赖:
json5
{
"dependencies": {
"@core/testing": "file:../../core/testing"
}
}2. 编写 Mock 类
在功能包的 src/test/mock/ 目录下创建 Mock 实现:
typescript
// packages/auth/src/test/mock/MockAuthRepository.ets
import { IAuthRepository } from "@shared/contracts";
export class MockAuthRepository implements IAuthRepository {
private loginResult: NetworkResult<Auth> | undefined;
/**
* 预设登录返回值
* @param result 预期返回的结果
*/
setLoginResult(result: NetworkResult<Auth>): void {
this.loginResult = result;
}
async loginByPassword(params: PasswordLoginRequest): Promise<NetworkResult<Auth>> {
if (this.loginResult) {
return this.loginResult;
}
// 默认返回成功
return new NetworkResult<Auth>({ code: 200, data: new Auth() });
}
}3. 编写测试用例
typescript
import { describe, beforeEach, afterEach, it, expect } from "@ohos/hypium";
import { TestContainerHelper } from "@core/testing";
import { AUTH_REPOSITORY_KEY, IAuthRepository } from "@shared/contracts";
import { MockAuthRepository } from "./mock/MockAuthRepository";
import LoginViewModel from "../viewmodel/LoginViewModel";
export default function loginViewModelTest() {
describe("LoginViewModel", () => {
const helper = new TestContainerHelper();
const mockAuth = new MockAuthRepository();
beforeEach(() => {
helper.setup();
helper.registerMock<IAuthRepository>(AUTH_REPOSITORY_KEY, mockAuth);
});
afterEach(() => {
helper.teardown();
});
it("登录成功应更新状态", 0, async () => {
const vm = new LoginViewModel();
await vm.login();
expect(vm.isLoginSuccess).assertTrue();
});
it("登录失败应显示错误信息", 0, async () => {
mockAuth.setLoginResult(new NetworkResult<Auth>({ code: 401 }));
const vm = new LoginViewModel();
await vm.login();
expect(vm.isLoginSuccess).assertFalse();
});
});
}高级用法
组合多个 Mock 服务
一个测试套件中可以同时 Mock 多个服务:
typescript
beforeEach(() => {
helper.setup();
helper.registerMock<IAuthRepository>(AUTH_REPOSITORY_KEY, mockAuth);
helper.registerMock<IUserRepository>(USER_REPOSITORY_KEY, mockUser);
helper.registerMock<AxiosHttpClient>(CoreServiceKeys.HttpClient, mockHttp);
});分层测试策略
根据测试目标选择不同的 Mock 层级:
ViewModel 测试 - Mock Repository 层:
typescript
// 测试 ViewModel 的状态管理和业务逻辑
helper.registerMock<IAuthRepository>(AUTH_REPOSITORY_KEY, mockAuthRepo);
const vm = new LoginViewModel();Repository 测试 - Mock DataSource / HttpClient 层:
typescript
// 测试 Repository 的数据转换逻辑
helper.registerMock<AxiosHttpClient>(CoreServiceKeys.HttpClient, mockHttpClient);
const repo = new AuthRepositoryImpl();可控的 Mock 行为
通过预设方法控制 Mock 的返回值,覆盖不同的测试场景:
typescript
const mockAuth = new MockAuthRepository();
// 场景 1:正常登录
mockAuth.setLoginResult(successResult);
// 场景 2:密码错误
mockAuth.setLoginResult(passwordErrorResult);
// 场景 3:网络异常
mockAuth.setLoginResult(networkErrorResult);最佳实践
1. 始终配对使用 setup / teardown
typescript
// 在 beforeEach 中 setup,在 afterEach 中 teardown
// 确保每个测试用例使用独立的子容器
beforeEach(() => helper.setup());
afterEach(() => helper.teardown());2. Mock 类实现完整接口
typescript
// 推荐:实现接口,编译期即可检查方法签名完整性
class MockAuthRepository implements IAuthRepository {
async loginByPassword(params: PasswordLoginRequest): Promise<NetworkResult<Auth>> {
return new NetworkResult<Auth>({ code: 200, data: new Auth() });
}
}3. Mock 数据集中管理
建议在功能包内统一管理 Mock 类和测试数据:
packages/auth/
└── src/test/
└── mock/
├── MockAuthRepository.ets # Mock 仓库实现
└── MockAuthData.ets # Mock 测试数据4. 只 Mock 必要的服务
子容器会自动继承父容器的所有服务,只需 Mock 当前测试关注的服务即可,无需重新注册所有依赖。
注意事项
- 调用顺序:必须先
setup()再registerMock(),否则会抛出异常 - 禁止重复 setup:连续调用两次
setup()而未teardown()会抛出异常 - 子容器不影响父容器:Mock 服务仅存在于子容器中,
teardown()后所有真实服务恢复正常 - 单例行为:通过
registerMock()注册的 Mock 实例在子容器中以单例模式存在