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

Flutter API异步错误导致getUsers接口失败测试未通过

TDD实现AuthenticationRemoteDataSource时getUsers失败测试问题

我正在用测试驱动开发(TDD)实现AuthenticationRemoteDataSource,这是和API交互的上层实现,包含createUser和getUsers两个方法。createUser的成功、失败测试都通过了,getUsers的成功测试也没问题,但失败状态的测试始终失败,找不到问题所在。

实现代码

AuthenticationRemoteDataSourceImplementation中的getUsers方法:

Future<List<UserModel>> getUsers() async {
    try {
      final response =
          await _client.get(Uri.parse('$kBaseUrl$kGetUsersEndpoint'));

      if (response.statusCode != 200) {
        throw (
          APIException(
            message: response.body,
            statusCode: response.statusCode,
          ),
        );
      }

      return List<DataMap>.from(jsonDecode(response.body) as List)
          .map((userdata) => UserModel.fromMap(userdata))
          .toList();
    } on APIException {
      rethrow;
    } catch (e) {
      throw APIException(message: e.toString(), statusCode: 505);
    }
  }

注:DataMap即Map<String, dynamic>,_client是http.Client实例,APIException是包含消息和状态码的自定义异常。

测试代码

getUsers的测试用例:

group('getUsers', () {
    const testList = [UserModel.empty()];
    test('Should return a [List<UserModel>] when the status code is 200',
        () async {
      // stubbing
      when(
        () => client.get(
          any(),
        ),
      ).thenAnswer((_) async =>
          http.Response(jsonEncode([testList.first.toMap()]), 200));

      // act
      final result = await remoteDataSource.getUsers();

      // assert
      expect(result, equals(testList));

      verify(() => client.get(Uri.parse('$kBaseUrl$kGetUsersEndpoint')))
          .called(1);
      verifyNoMoreInteractions(client);
    });

    // 失败的测试用例
    test('Should throw an [APIException] when the status code is not 200',
        () async {
      // stubbing
      when(() => client.get(any())).thenAnswer(
        (_) async => http.Response(
          'Server is down',
          505,
        ),
      );

      // act
      final methodCall = remoteDataSource.getUsers;

      // assert
      expect(
        () => methodCall(),
        throwsA(
          const APIException(
            message: 'Server is down',
            statusCode: 505,
          ),
        ),
      );

      verify(
        () => client.get(
          Uri.parse('$kBaseUrl$kGetUsersEndpoint'),
        ),
      ).called(1);

      verifyNoMoreInteractions(client);
    });
  });

报错信息

Expected: throws APIException:<APIException(Server is down, 505)>
  Actual: <Closure: () => Future<List<UserModel>>>
   Which: threw APIException:<APIException((APIException(Server is down, 505)), 505)>
          stack package:cleanarchtdd/src/authentication/data/datasources/authentication_remote_data_source.dart 78:7  AuthenticationRemoteDataSourceImplementation.getUsers
                ===== asynchronous gap ===========================
                package:matcher                                                                                       expect
                package:flutter_test/src/widget_tester.dart 454:18                                                    expect
                test\src\authentication\data\datasources\authentication_remote_data_source_test.dart 147:7            main.<fn>.<fn>

package:matcher                                                                             expect
package:flutter_test/src/widget_tester.dart 454:18                                          expect
test\src\authentication\data\datasources\authentication_remote_data_source_test.dart 147:7  main.<fn>.<fn>

我试过在when和verify里加async关键字,但引发了其他错误,而且这个测试的写法和createUser的失败测试完全一致。


问题原因及修复方案

  1. 核心问题:throw语句的多余括号
    从报错信息里的APIException((APIException(Server is down, 505)), 505)能看出,实际抛出的异常是把原本的APIException作为了新异常的message。原因是getUsers里的throw语句多了一对括号:

    // 错误写法:抛出的是一个包含APIException的元组
    throw (
      APIException(
        message: response.body,
        statusCode: response.statusCode,
      ),
    );
    

    这会导致抛出的不是APIException实例,而是一个元组,这个元组会被后续的catch (e)捕获,然后被包装成一个新的APIException,message就是元组的字符串形式。

    修复成:

    // 正确写法:直接抛出APIException实例
    throw APIException(
      message: response.body,
      statusCode: response.statusCode,
    );
    
  2. 异步测试的写法优化
    测试中的异步异常断言,建议用expectLater来确保异步操作完成后再断言,避免时序问题:

    // 替换原来的expect代码
    await expectLater(
      methodCall(),
      throwsA(const APIException(message: 'Server is down', statusCode: 505)),
    );
    

完成这两处修改后,失败的测试应该就能通过了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 17:06:05