flutter 在隔离菌株中使用类的特定示例

q7solyqu  于 2023-05-19  发布在  Flutter
关注(0)|答案(3)|浏览(209)

我通过compute()方法使用一个isolate来从一个API(大约10k个条目)中获取、解析和排序数据。
我的方法getAllCards()是在一个类YgoProRepositoryImpl中定义的,这个类有一个我的远程数据源类YgoProRemoteDataSource的示例,在这个类中定义了调用我的API的方法(它是一个简单的GET请求)。

代码示例

ygopro_repository_impl.dart

class YgoProRepositoryImpl implements YgoProRepository {
  final YgoProRemoteDataSource remoteDataSource;

  // ...

  YgoProRepositoryImpl({
    required this.remoteDataSource,
    // ...
  });

  // ...

  static Future<List<YgoCard>> _fetchCards(_) async {
    // As I'm inside an isolate I need to re-setup my locator
    setupLocator();
    final cards = await sl<YgoProRemoteDataSource>()
        .getCardInfo(GetCardInfoRequest(misc: true));
    cards.sort((a, b) => a.name.compareTo(b.name));
    return cards;
  }

  @override
  Future<List<YgoCard>> getAllCards() async {
    final cards = await compute(_fetchCards, null);
    return cards;
  }

  // ...
}

service_locator.dart

import 'package:get_it/get_it.dart';

import 'data/api/api.dart';
import 'data/datasources/remote/ygopro_remote_data_source.dart';
import 'data/repository/ygopro_repository_impl.dart';
import 'domain/repository/ygopro_repository.dart';

final sl = GetIt.instance;

void setupLocator() {
  // ...

  _configDomain();
  _configData();

  // ...

  _configExternal();
}

void _configDomain() {
  //! Domain
  
  // ...

  // Repository
  sl.registerLazySingleton<YgoProRepository>(
    () => YgoProRepositoryImpl(
      remoteDataSource: sl(),
      // ...
    ),
  );
}

void _configData() {
  //! Data
  // Data sources
  sl.registerLazySingleton<YgoProRemoteDataSource>(
    () => YgoProRemoteDataSourceImpl(sl<RemoteClient>()),
  );

  // ...
}

void _configExternal() {
  //! External
  sl.registerLazySingleton<RemoteClient>(() => DioClient());
  
  // ...
}

代码工作正常,但getAllCards()不可测试,因为我不能在隔离中注入YgoProRemoteDataSource的模拟类,因为它总是从我的服务定位器获得引用。
如何才能不依赖服务定位器将YgoProRemoteDataSource注入隔离菌株并使getAllCards()可测试?

brjng4g3

brjng4g31#

做了一个更严重的尝试,请参阅回购:https://github.com/maxim-saplin/compute_sl_test_sample
从本质上讲,在Flutter/Dart的当前状态下,你不能跨隔离边界传递闭包或包含闭包的类(但当Dart中的新功能登陆Flutter时,这可能会改变https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528)。这意味着,如果您不希望任何测试代码成为发布构建的一部分,则无法传递服务定位器(包含闭包)或欺骗隔离以通过闭包示例化定位器的测试版本。但是,您可以轻松地将数据源示例传递给隔离,以便在其入口点作为参数使用。
此外,我不认为要求isolate重新构建整个服务定位器是有意义的。compute()背后的整个思想是创建一个短的离开隔离,运行计算,返回结果并终止隔离。初始化定位器是最好避免的开销。此外,compute()的整个概念似乎与应用程序的其余部分尽可能隔离。
您可以克隆存储库并运行测试。关于样品的几句话:

  • 基于Flutter计数器启动器应用程序
  • lib/classes.dart重新创建您提供的代码段
  • test/widget_test.dart验证YgoProRepositoryImpl在隔离运行伪造版本的数据源时工作正常
  • YgoProRemoteDataSourceImpl模拟真实的实现,位于classes.dart和YgoProRemoteDataSourceFake模拟测试版本
  • 在flutter_test下运行隔离需要在tester.runAsync()中 Package 测试主体,以便真实的异步执行(而不是测试默认使用的假异步,并依赖于泵来推进测试时间)。在这种模式下运行测试可能会很慢(实际上有0.5秒的等待时间),以一种不使用compute()或在许多测试中没有测试的方式来构建测试是合理的

classes.dart

import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

class YgoCard {
  YgoCard(this.name);

  final String name;
}

abstract class YgoProRemoteDataSource {
  Future<List<YgoCard>> getCardInfo();
}

class YgoProRemoteDataSourceImpl extends YgoProRemoteDataSource {
  @override
  Future<List<YgoCard>> getCardInfo() {
    return Future.delayed(Duration.zero,
        () => List.generate(5, (index) => YgoCard("Impl $index")));
  }
}

abstract class YgoProRepository {
  Future<List<YgoCard>> getAllCards();
}

class YgoProRepositoryImpl implements YgoProRepository {
  final YgoProRemoteDataSource remoteDataSource;

  YgoProRepositoryImpl({
    required this.remoteDataSource,
  });

  static Future<List<YgoCard>> _fetchCards(
      YgoProRemoteDataSource dataSource) async {
    final cards = await dataSource.getCardInfo();
    cards.sort((a, b) => a.name.compareTo(b.name));
    return cards;
  }

  @override
  Future<List<YgoCard>> getAllCards() async {
    final cards = await compute(_fetchCards, remoteDataSource);
    return cards;
  }
}

void setupLocator() {
  sl.registerLazySingleton<YgoProRepository>(
    () => YgoProRepositoryImpl(
      remoteDataSource: sl(),
    ),
  );

  sl.registerLazySingleton<YgoProRemoteDataSource>(
    () => YgoProRemoteDataSourceImpl(),
  );
}

widget_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:test_sample/classes.dart';

import 'package:test_sample/main.dart';

void main() {
  setUpAll(() async {
    setupFakeLocator();
  });

  testWidgets('Test mocked data source', (WidgetTester tester) async {
    // Wrapping with runAync() is required to have real async in place
    await tester.runAsync(() async {
      await tester.pumpWidget(const MyApp());
      // Let the isolate spawned by compute() complete, Debug run might require longer wait
      await Future.delayed(const Duration(milliseconds: 500));
      await tester.pumpAndSettle();
      expect(find.text('Fake 9'), findsOneWidget);
    });
  });
}

class YgoProRemoteDataSourceFake extends YgoProRemoteDataSource {
  @override
  Future<List<YgoCard>> getCardInfo() {
    return Future.delayed(Duration.zero,
        () => List.generate(10, (index) => YgoCard("Fake $index")));
  }
}

void setupFakeLocator() {
  sl.registerLazySingleton<YgoProRepository>(
    () => YgoProRepositoryImpl(
      remoteDataSource: sl(),
    ),
  );

  sl.registerLazySingleton<YgoProRemoteDataSource>(
    () => YgoProRemoteDataSourceFake(),
  );
}
carvr3hs

carvr3hs2#

你真的需要测试getCards()函数吗?你到底在测试什么?compute可以工作,当然希望Dart SDK团队对此进行测试。
剩下的就是_fetchCards()setupLocator()也不需要测试,这是测试逻辑的前提条件。无论如何,您都希望更改测试的设置。
所以你真正想测试的是抓取和排序。将其重新构造成一个可测试的静态函数,并预先设置定位器。在上面放一个@visibleForTesting注解。
顺便说一下,根据您在服务定位器中绑定的数量,这可能是之后仅使用一个存储库的巨大开销。
示例:

static Future<List<YgoCard>> _fetchCards(_) async {
    // As I'm inside an isolate I need to re-setup my locator
    setupLocator();
    return reallyFetchCards();
  }

  @visibleForTesting
  static Future<List<YgoCard>> reallyFetchCards() async {
    final cards = await sl<YgoProRemoteDataSource>()
        .getCardInfo(GetCardInfoRequest(misc: true));
    cards.sort((a, b) => a.name.compareTo(b.name));
    return cards;
  }

  @override
  Future<List<YgoCard>> getAllCards() async {
    final cards = await compute(_fetchCards, null);
    return cards;
  }

测试:

// Setup SL and datasource
...

final cards = await YgoProRepositoryImpl.reallyFetchCrads();

// Expect stuff
cu6pst1q

cu6pst1q3#

据我所知,您有两种选择,要么通过参数注入static Future<List<YgoCard>> _fetchCards(_) async所需的依赖项,要么在定位器本身中模拟对象。我会选择第一个选项,并有如下内容:

static Future<List<YgoCard>> _fetchCards(_,YgoProRemoteDataSource remote) async {
    // No need to set up locator as you passed the needed dependencies
    // setupLocator();
    final cards = await remote
        .getCardInfo(GetCardInfoRequest(misc: true));
    cards.sort((a, b) => a.name.compareTo(b.name));
    return cards;
  }

  @override
  Future<List<YgoCard>> getAllCards() async {
    final cards = await compute(_fetchCards, null);
    return cards;
  }

编辑

刚刚更新了答案,因为在这里编辑这个比在评论中更容易...
嗯,我能想到的唯一解决办法是将setupLocator()函数作为参数传递给类YgoProRepositoryImpl:

final Function setupLocator;
  YgoProRepositoryImpl({
    required this.remoteDataSource,
required this.setupLocator;
    // ...
  });

通过这种方式,您可以传递一个设置模拟类的模拟或service_locator.dart的真实的setupLocator。这可能不太优雅。但它应该使它可测试,因为现在你可以模拟设置和它没有硬编码的功能

相关问题