You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试内部含API调用的函数并保证单元测试覆盖率

问题描述

正在对内部调用外部API的fetchTelehealthDetail方法编写单元测试,对应业务类代码如下:

class TelehealthController extends GetxController {
  final RxBool loadingDetails = true.obs;
  TelehealthController();
  TelehealthsClient physician = Get.put(TelehealthsClient(Dio()));

  late TelehealthResponse details;
  late List<TelehealthData>? datadetails = details.data;

  int? get id => details.data!.first.physicianId;
  String get telehealth =>
      details.data!.first.defaulttelehealthinstructions.toString();

  @override
  void onInit() async {
    super.onInit();
    //Here each API call needs to Initialize
    await fetchTelehealthDetail();
  }

  Future<TelehealthResponse> fetchTelehealthDetail() async {
    loadingDetails.value = true;
    final response = await physician.getTelehealthData(); //mock
    loadingDetails.value = false;
    details = TelehealthResponse(data: response);
    int count = details.data!.length;
    loadingDetails.value = false;
    return details;
  }
}

按照单元测试规范,所有外部API、数据库调用都需要Mock,避免触发真实网络请求。当前遇到的核心问题是:调用controller实例的fetchTelehealthDetail方法时总会触发真实API请求,无法正确Mockphysician.getTelehealthData()调用。
之前尝试直接Mock整个TelehealthController类,为fetchTelehealthDetail返回模拟响应,但这种方式下真实业务代码不会执行,flutter test --coverage生成的覆盖率报告仅为4%,也无法验证id、telehealth等getter的逻辑正确性。
原有测试Mock代码如下:

class MockTelehealthClients extends Mock implements TelehealthsClient {}

class MockTelehealthController extends Mock implements TelehealthController {}

void main() async {
  TestWidgetsFlutterBinding.ensureInitialized();
  List<TelehealthData> _listofMockTelehealthData = [
    TelehealthData(
        physicianId: 12345,
        defaulttelehealthinstructions:
            "At the time of your appointment, click this link to join your video visit: https://zoom.us/j/a-random-zoom-link"),
    TelehealthData(
        physicianId: 890,
        defaulttelehealthinstructions:
            "At the time of your appointment, click this link to join your video visit: https://zoom.us/another-zoom-link"),
  ];
  var _telehealthResponse = TelehealthResponse(data: _listofMockTelehealthData);
  var _mockTelehealthClient = MockTelehealthClients();
  var sut = TelehealthController();
  var _mockTelehealthController = MockTelehealthController();


//mocking using Mocktail
 when(() => _mockTelehealthClient.getTelehealthData()).thenAnswer(
        (invocation) => Future.delayed(Duration(seconds: 1), () async {
              return _listofMockTelehealthData;
            }));

    when((() => _mockTelehealthController.fetchTelehealthDetail()))
        .thenAnswer((invocation) {
      return Future.delayed(Duration(seconds: 1), () {
        return _telehealthResponse;
      });
    });
}

需要可行的解决方案,在正确Mock外部API调用的同时保证代码覆盖率统计准确。

问题根因

原有测试代码存在两个核心错误:

  • 实例化真实TelehealthController时,构造函数内部直接通过Get.put注入了真实的TelehealthsClient实例,没有替换成Mock对象,后续所有对Mock客户端的Stub都不会作用到真实Controller持有的客户端实例上
  • 额外Mock整个TelehealthController的操作完全多余,Mock后的Controller不会执行真实业务逻辑,自然无法统计覆盖率、验证内部getter逻辑
  • 原业务代码存在时序隐患:late List<TelehealthData>? datadetails = details.data;在Controller初始化时就会访问还未赋值的late变量details,运行时会直接抛出异常
解决方案

第一步:改造Controller支持依赖注入

不要在Controller内部硬编码初始化依赖,改为通过构造函数传入依赖,方便测试时替换Mock对象,同时修复原有变量初始化的时序问题,改造后Controller代码如下:

class TelehealthController extends GetxController {
  final RxBool loadingDetails = true.obs;
  // 依赖通过构造函数注入,不再内部硬编码初始化
  final TelehealthsClient physician;

  TelehealthController({required this.physician});

  late TelehealthResponse details;
  late List<TelehealthData>? datadetails;

  int? get id => details.data!.first.physicianId;
  String get telehealth =>
      details.data!.first.defaulttelehealthinstructions.toString();

  @override
  void onInit() async {
    super.onInit();
    datadetails = [];
    await fetchTelehealthDetail();
  }

  Future<TelehealthResponse> fetchTelehealthDetail() async {
    loadingDetails.value = true;
    final response = await physician.getTelehealthData();
    loadingDetails.value = false;
    details = TelehealthResponse(data: response);
    datadetails = details.data;
    int count = details.data!.length;
    loadingDetails.value = false;
    return details;
  }
}

生产环境初始化Controller时,传入真实的客户端实例即可:Get.put(TelehealthController(physician: TelehealthsClient(Dio())))

第二步:编写正确的测试代码

不需要Mock被测的Controller本身,只需要Mock外部依赖TelehealthsClient,在初始化Controller时传入Mock客户端实例,再对Mock客户端的方法做Stub即可,测试代码示例:

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:get/get.dart';

// 只Mock外部依赖,不Mock被测类
class MockTelehealthClients extends Mock implements TelehealthsClient {}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  late MockTelehealthClients mockTelehealthClient;
  late TelehealthController sut;

  // 每个测试用例执行前重置环境
  setUp(() {
    Get.reset(); // 重置GetX容器,避免前序测试的实例残留
    mockTelehealthClient = MockTelehealthClients();
    // 初始化被测Controller时,传入Mock的客户端实例
    sut = TelehealthController(physician: mockTelehealthClient);
    Get.put<TelehealthController>(sut);
  });

  test('fetchTelehealthDetail should call api, update state and getters correctly', () async {
    // 准备Mock返回数据
    final List<TelehealthData> mockTelehealthData = [
      TelehealthData(
          physicianId: 12345,
          defaulttelehealthinstructions:
              "At the time of your appointment, click this link to join your video visit: https://zoom.us/j/a-random-zoom-link"),
      TelehealthData(
          physicianId: 890,
          defaulttelehealthinstructions:
              "At the time of your appointment, click this link to join your video visit: https://zoom.us/another-zoom-link"),
    ];

    // Stub Mock客户端的方法,单元测试不需要加多余延迟模拟网络等待
    when(() => mockTelehealthClient.getTelehealthData())
        .thenAnswer((_) async => mockTelehealthData);

    // 初始状态校验
    expect(sut.loadingDetails.value, true);

    // 执行被测方法
    final result = await sut.fetchTelehealthDetail();

    // 校验外部API仅被调用1次
    verify(() => mockTelehealthClient.getTelehealthData()).called(1);
    // 校验loading状态正确变更
    expect(sut.loadingDetails.value, false);
    // 校验返回结果正确
    expect(result.data, mockTelehealthData);
    expect(sut.details.data, mockTelehealthData);
    expect(sut.datadetails, mockTelehealthData);
    // 校验getter逻辑正确
    expect(sut.id, 12345);
    expect(sut.telehealth, "At the time of your appointment, click this link to join your video visit: https://zoom.us/j/a-random-zoom-link");
  });
}

关键注意点

  • 单元测试永远不要Mock被测类本身,只Mock被测类的外部依赖,否则真实代码不会执行,覆盖率和逻辑验证都会失效
  • 依赖注入是可测试性的核心,不要在类内部硬编码初始化外部依赖,通过构造函数传入依赖可以方便测试时无缝替换Mock对象
  • 测试用例执行前后要重置GetX的依赖容器,避免不同测试用例之间的实例污染
  • 单元测试中的Mock方法返回不需要加多余的延迟,否则会拖慢测试执行速度

内容的提问来源于stack exchange,提问作者Sanjay Sanjel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 03:54:31