Skip to content

测试工具

概述

测试工具模块(@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 当前测试关注的服务即可,无需重新注册所有依赖。

注意事项

  1. 调用顺序:必须先 setup()registerMock(),否则会抛出异常
  2. 禁止重复 setup:连续调用两次 setup() 而未 teardown() 会抛出异常
  3. 子容器不影响父容器:Mock 服务仅存在于子容器中,teardown() 后所有真实服务恢复正常
  4. 单例行为:通过 registerMock() 注册的 Mock 实例在子容器中以单例模式存在

下一步