欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里插入图片描述

前言

在软件开发中,单元测试(Unit Testing)是保证代码质量的基石。然而,在测试某个具体的业务逻辑(如 UserService)时,我们往往会遇到各种外部依赖,比如数据库查询、网络请求、设备传感器等。

如果直接调用真实的 DatabaseHttpClient,不仅测试速度慢,而且容易因为网络抖动或环境问题导致测试失败。此外,我们很难复现一些极端场景(如 500 服务器错误、数据库连接超时)。

Mockito 就是为了解决这个问题而生的。它允许我们创建对象的 Mock(替身),并精确控制这些替身的行为(Stubbing)和验证它们的交互(Verification)。

在 OpenHarmony 应用开发中,使用 mockito 可以让我们在开发机(Host)上就能快速验证大部分业务逻辑,通过后再部署到鸿蒙真机进行集成测试,极大地提高了开发效率。

一、核心概念与工作原理

1.1 什么是 Mock?

Mock 对象是真实对象的“傀儡”。它继承自真实类,但不会执行真实类的任何代码。相反,它记录了所有对它的调用,并根据你的配置返回预设的值。

1.2 Mockito 的黑魔法:代码生成 (Codegen)

早期版本的 Mockito 依赖 noSuchMethod 和反射(Mirrors),但在 Flutter 中禁止使用反射(为了 AOT 优化)。因此,现在的 mockito 配合 build_runner 使用,在编译时生成 Mock 类。

@GenerateMocks

生成

使用

验证

RealClass.dart

build_runner

RealClass.mocks.dart

单元测试

Mock 对象

1.3 核心操作三部曲

  1. Stubbing (桩): when(mock.method()).thenReturn(value) —— “当调用这个方法时,请返回这个值”。
  2. Execution (执行): 运行被测代码,被测代码会调用 Mock 对象。
  3. Verification (验证): verify(mock.method()) —— “验证这个方法是否被调用过,且参数正确”。

二、集成与基础用法

2.1 添加依赖

mockito 通常只在 dev_dependencies 中使用。

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.6.3
  build_runner: ^2.4.0

2.2 定义被测类

假设我们有一个从 API 获取用户信息的服务:

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

class UserApi {
  final http.Client client;

  UserApi(this.client);

  Future<String> fetchUserName(int id) async {
    final response = await client.get(Uri.parse('https://api.example.com/users/$id'));
    if (response.statusCode == 200) {
      return jsonDecode(response.body)['name'];
    } else {
      throw Exception('Failed to load user');
    }
  }
}

2.3 生成 Mock 类

创建一个测试文件,使用注解 @GenerateMocks

// user_api_test.dart
import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'user_api.dart';
// 引入生成的文件
import 'user_api_test.mocks.dart';

([http.Client])
void main() {
  // ...
}

然后在终端运行命令生成代码:

flutter pub run build_runner build

2.4 编写测试用例

void main() {
  late MockClient mockClient;
  late UserApi userApi;

  setUp(() {
    mockClient = MockClient();
    userApi = UserApi(mockClient);
  });

  test('returns name if the http call completes successfully', () async {
    // 1. Stubbing: 模拟 http.Client 返回 200 OK
    when(mockClient.get(Uri.parse('https://api.example.com/users/1')))
        .thenAnswer((_) async => http.Response('{"name": "张三"}', 200));

    // 2. Execution
    final name = await userApi.fetchUserName(1);

    // 3. Verification: 验证结果
    expect(name, '张三');
    // 验证 get 方法确实被调用了一次
    verify(mockClient.get(Uri.parse('https://api.example.com/users/1'))).called(1);
  });

  test('throws an exception if the http call completes with an error', () {
    // 1. Stubbing: 模拟返回 404
    when(mockClient.get(Uri.parse('https://api.example.com/users/1')))
        .thenAnswer((_) async => http.Response('Not Found', 404));

    // 2. Execution & Verification
    expect(userApi.fetchUserName(1), throwsException);
  });
}

在这里插入图片描述
在这里插入图片描述

三、进阶技巧

3.1 参数匹配 (Argument Matchers)

有时候我们不关心具体的参数值,只关心参数类型。

// 匹配任何 Uri
when(mockClient.get(any)).thenAnswer(...)

// 命名参数匹配
when(obj.method(foo: anyNamed('foo'))).thenReturn(...)

// 捕获参数进行验证
test('captures arguments', () {
  // ... trigger ...
  // 验证调用并捕获参数
  final captured = verify(mockClient.get(captureAny)).captured;
  print(captured.first); // 输出 Uri 对象
});

3.2 顺序验证 (InOrder)

验证多个方法的调用顺序。

test('verify interaction order', () {
  final cat = MockCat();
  cat.eat();
  cat.sleep();

  verifyInOrder([
    cat.eat(),
    cat.sleep(),
  ]);
});

3.3 模拟异常与重试逻辑

开发鸿蒙应用时,网络波动是常态。我们可以 Mock 抛出异常来测试 App 的重试机制是否工作。

test('测试重试逻辑', () async {
  // 第一次调用抛出异常,第二次成功
  when(mockApi.getData())
      .thenThrow(Exception('Network Error'))
      .thenAnswer((_) async => 'Success');

  // 调用业务逻辑...
});

在这里插入图片描述

四、OpenHarmony 适配与实战:模拟鸿蒙原生通道 (Platform Channel)

在 OpenHarmony 开发中,我们经常需要通过 MethodChannel 调用系统的原生能力(如获取电池电量、调用 HiLog)。在写 UI 测试或逻辑测试时,我们不能真的去调底层,因为测试环境可能只是一个 Dart VM。

我们可以 Mock MethodChannel 的 handlers。

4.1 场景:电池电量获取

假设我们有一个 BatteryService

import 'package:flutter/services.dart';

class BatteryService {
  static const platform = MethodChannel('ohos.samples.battery');

  Future<String> getBatteryLevel() async {
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      return '当前电量 $result%';
    } on PlatformException catch (e) {
      return "获取电量失败: '${e.message}'.";
    }
  }
}

4.2 编写测试

Flutter 官方提供了 TestDefaultBinaryMessenger 来模拟底层消息。但在单元测试层面,mockito 也可以派上用场,或者我们可以直接 mock 对 BatteryService 的依赖。

这里我们展示如何使用 mockito 测试一个依赖 BatteryServiceViewModel

// battery_view_model.dart
class BatteryViewModel {
  final BatteryService _service;
  String status = '未知';

  BatteryViewModel(this._service);

  Future<void> refresh() async {
    status = await _service.getBatteryLevel();
  }
}

// battery_view_model_test.dart
([BatteryService])
import 'battery_view_model_test.mocks.dart';

void main() {
  test('ViewModel updates status from service', () async {
    final mockService = MockBatteryService();
    final viewModel = BatteryViewModel(mockService);

    // Stubbing: 模拟鸿蒙设备返回了 100% 电量
    when(mockService.getBatteryLevel())
        .thenAnswer((_) async => '当前电量 100%');

    await viewModel.refresh();

    expect(viewModel.status, '当前电量 100%');
  });
}

4.3 为什么这对鸿蒙开发重要?

鸿蒙设备的 API(如 @ohos.batteryInfo)只能在真机或模拟器运行。如果你的 Dart 代码中混杂了原生调用,一旦离开真机环境就会报错。

通过 Dependency Injection (依赖注入) 配合 Mockito,你可以让 Dart 业务逻辑与鸿蒙原生 API 解耦。

  • 开发时:在 PC 上运行测试,Mock 掉鸿蒙 API,开发效率极高。
  • 运行时:注入真实的 BatteryService,调用鸿蒙底层。

五、总结

mockito 是测试驱动开发(TDD)的利器。它让我们能够隔离关注点,专注于当前模块的逻辑,而不必担心外部世界的复杂性。

对于 OpenHarmony 开发者,掌握 Mock 技术意味着你可以构建出架构更清晰、可测试性更强的应用。无论是模拟网络、数据库,还是模拟鸿蒙特有的系统服务,mockito 都能帮你轻松搞定。

最佳实践

  1. 多用接口:针对 Interface 编程,而不是针对 Implementation。Mock 接口比 Mock 具体类更容易且风险更小。
  2. 不要 Mock 数据类:对于 DTO(Data Transfer Object)或简单的实体类,直接 new 一个真的对象,不要 Mock 它。
  3. 保持测试简单:如果你的 Mock 配置(when…then…)写了几十行,说明被测代码可能耦合太重,需要重构了。

六、完整实战示例:带超时的网络请求模拟器

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

// 1. 业务接口
abstract class NetworkService {
  Future<String> request(String path);
}

// 2. 模拟生成的 Mock 类
class MockNetworkService extends Mock implements NetworkService {
  
  Future<String> request(String? path) => super.noSuchMethod(
        Invocation.method(#request, [path]),
        returnValue: Future.value(""),
      ) as Future<String>;
}

// 3. 业务逻辑类
class DataManager {
  final NetworkService _service;
  DataManager(this._service);

  Future<String> safeRequest() async {
    try {
      // 模拟 2 秒延迟外的逻辑
      return await _service.request('/data').timeout(Duration(seconds: 1));
    } catch (e) {
      return "Timeout or Error";
    }
  }
}

void main() {
  test('模拟请求超时场景', () async {
    final mockService = MockNetworkService();
    final manager = DataManager(mockService);

    // Stubbing: 让请求延迟 2 秒返回,从而触发业务层的超时
    when(mockService.request(any)).thenAnswer((_) async {
      await Future.delayed(Duration(seconds: 2));
      return "Delayed Data";
    });

    final result = await manager.safeRequest();

    expect(result, "Timeout or Error");
    verify(mockService.request('/data')).called(1);
  });
}

在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐