Flutter 单元测试中需要 Class 类型参数的方法调用会导致 MissingStubError

问题描述 投票:0回答:1

我正在尝试对存储库类进行单元测试。在存根

when
中,我调用了一个需要
GamesParams
类类型参数的方法。这会产生
MissingStubError
错误。但是,如果我将映射参数 的结果(请参阅下面的
参数
中的 toApiParams
硬编码为
String
请求方法中的
client.get
,则测试会成功。我该如何解决这个问题?

结果

Expected: Right<Failure, GamesEntity>:<Right(GamesEntity([], false))>
Actual: MissingStubError:<MissingStubError: 'getGames'
        No stub was found which matches the arguments of this method call:
        getGames({params: Instance of 'GamesParams'})

测试

import 'games_repository_imp_test.mocks.dart';
import 'mock_data/actual_games_data.dart';
import 'mock_data/expected_games_data.dart';

@GenerateMocks([GamesImpApi])
void main() {
  // Api Params
  int gamesEntityParams = 1;
  GamesParams gamesParams = GamesParams.getPage(gamesEntityParams);

  // Mocked GamesImpApi class
  late MockGamesImpApi mockApi;

  // Our Repository class that we need to test it.
  // The dependency for this class will get from the mocked GamesImpApi class
  // not from real GamesImpApi class
  late AbstractGamesRepository gamesRepositoryImp;

  setUp(() {
    mockApi = MockGamesImpApi();
    gamesRepositoryImp = GamesRepositoryImp(source: mockApi);
  });

  group('Test Games Repository Implementation', () {
    test('Get All Games - Failed Case, Empty Or Null Api response', () async {
      when(mockApi.getGames(params: gamesParams))
          .thenAnswer((realInvocation) async {
        return actualGamesFailedOrEmptyListData;
      });
      Object result;
      try {
        result = await gamesRepositoryImp.getGames(gamesEntityParams);
      } catch (e) {
        result = e;
      }

      expect(result, expectedGamesEmptyListData);
    });

    test('Get All Games - Success Case', () async {
      when(mockApi.getGames(params: gamesParams))
          .thenAnswer((realInvocation) async {
        return actualGamesListData;
      });
      Object result;
      try {
        result = await gamesRepositoryImp.getGames(gamesEntityParams);
      } catch (e) {
        result = e;
      }
      expect(result, expectedGamesListData);
    });
  });
}

API

class GamesImpApi extends AbstractGamesApi {
  final Dio client;

  GamesImpApi(this.client);

  @override
  Future<GamesResponseModel<GameModel>> getGames({
    required GamesParams params,
  }) {
    return clientExecutor<GamesResponseModel<GameModel>>(
      execute: () async {
        final res = await client.get(
          NetworkUtils.getApiPath(
            GamesImpApiConst.games,
            params: params,
          ),
        );
        return GamesResponseModel.fromJson(
          res.data,
          (json) => GameModel.fromJson(json as Map<String, dynamic>),
        );
      },
    );
  }
}

class GamesImpApiConst {
  GamesImpApiConst._();

  static const games = '/games';
}

参数

class GamesParams extends AbstractParamsModel {
  final int page;
  final int pageSize;
  final List<int> platforms;
  final GamesParamsDates dates;
  final GamesParamsOrder ordering;

  GamesParams({
    required this.page,
    required this.pageSize,
    required this.platforms,
    required this.dates,
    required this.ordering,
  });

  @override
  String toApiParams({bool fromStart = false}) {
    return '${fromStart ? '?' : '&'}'
        'page=$page'
        '&pageSize=$pageSize'
        '&platforms=${platforms.join(',')}'
        '&dates=${dates.toValue()}'
        '&ordering=${ordering.toValue()}';
  }

  /// Parameter to set page only while other values are set with the defaults.
  factory GamesParams.getPage(int page) {
    var date = DateTime.now();
    var startDate = DateTime(date.year - 1);

    return GamesParams(
      page: page,
      pageSize: 20,
      platforms: [187],
      dates: GamesParamsDates.range(
        startDate: startDate,
        date: date,
      ),
      ordering: GamesParamsOrder(
        type: GamesParamsOrderType.released,
        sort: GamesParamsOrderSort.desc,
      ),
    );
  }
}

公用事业

class NetworkUtils {
  static String getApiKeyParam() => '?key=${NetworkConst.apiKey}';

  static String getApiPath(
    String path, {
    AbstractParamsModel? params,
  }) {
    return '${NetworkConst.apiUrl}'
        '$path'
        '${getApiKeyParam()}'
        '${params != null ? params.toApiParams() : ''}';
  }
}

存储库

class GamesRepositoryImp implements AbstractGamesRepository {
  final GamesImpApi source;

  GamesRepositoryImp({
    required this.source,
  });

  @override
  Future<Either<Failure, GamesEntity>> getGames(int page) async {
    try {
      final result = await source.getGames(
        params: GamesParams.getPage(page),
      );

      var dataMapped = GamesEntity(
        results: (result.results ?? []).map((e) => e.mapToEntity()).toList(),
        hasMore: result.next != null,
      );

      return Right(dataMapped);
    } on ApiException catch (e) {
      return Left(
        ServerFailure(
          e.response?.message ?? e.error?.message ?? 'Something went wrong',
        ),
      );
    } on Failure catch (e) {
      return Left(ServerFailure(e.errorMessage));
    }
  }
}
flutter unit-testing mockito
1个回答
0
投票

TL;博士

您可能想要注册您的存根以专门匹配

GamesParam
page
成员:

  when(
    mockApi.getGames(
      params: argThat(
        isA<GamesParams>().having(
          (params) => params.page,
          'page',
          gamesEntityParams,
        ),
      ),
    ),
  ).thenAnswer(...);

详情

如果您使用 Mockito 注册了一个存根,但得到了

MissingStubError
,这意味着调用模拟函数的参数与存根期望的参数不匹配。

如果您没有显式指定参数匹配器,Mockito 将假定您希望基于对象相等性进行匹配。如果您的参数不覆盖

operator ==
,则默认情况下相等意味着对象同一性(即,仅当对象是完全相同的实例时,对象才相等)。正如我在评论中指出的,您的存根需要一个
GamesParam
参数,但
GamesParam
不会覆盖
operator ==
,因此它将仅匹配使用相同
mockApi.getGames
对象调用
GamesParam
,但
GamesRepositoryImp
调用
 source.getGames(params: GamesParams.getPage(page))
GamesParams.getPage
工厂构造函数总是返回一个新的
GamesParam
对象。

operator ==
添加到
GamesParam
没有帮助,可能是因为您的实现(您尚未显示)不正确。我猜你做了类似的事情:

@override
bool operator ==(Object other) {
  if (other is! GamesParam) {
    return false;
  }

  return page == other.page &&
    pageSize == other.pageSize &&
    platforms == other.platforms &&
    dates == other.dates &&
    ordering == other.ordering;
}

但这会带来一些问题:

  • dates
    ordering
    成员分别是
    GamesParamsDates
    GamesParamsOrder
    对象,因此这些类也需要
    operator ==
    实现,或者
    GamesParam
    operator ==
    需要知道如何比较它们的内部结构。

  • platforms
    是一个
    List<int>
    ,因此您需要 执行深度
    List
    相等
    GamesParam
    operator ==
    需要手动比较元素。

  • GamesParams.getPage
    工厂构造函数使用
    dates
    初始化
    DateTime.now()
    。因此,如果使用
    dates
    来确定相等性,则对
    GamesParams.getPage
    的两次调用永远无法可靠地返回比较相等的不同对象。

    您可以通过以下任一方式解决此问题:

    • 更改
      GamesParams
      operator ==
      以忽略日期。
    • 使用
      package:clock
      允许测试指定“现在”应该是什么:
      void main() {
        test('...', () {
          withClock<void>(
            Clock.fixed(DateTime.now()),
            () {
              // This depends on the clock so must be executed within the
              // `withClock` callback.
              GamesParams gamesParams = GamesParams.getPage(gamesEntityParams);
      
              // Other test code goes here.
            },
          ),
        });
      }
      
      并在各处将
      DateTime.now()
      替换为
      clock.now()

现在,所有这些工作可能比您想要的要多得多,因此您最好使用不需要严格平等的不同

Matcher
来注册您的存根。

© www.soinside.com 2019 - 2024. All rights reserved.