在 isolate 中使用 class 的特定实例
Use a specific instance of a class inside an isolate
我正在通过 compute()
方法使用隔离来从 API(大约 10k 个条目)中获取、解析和排序数据。
我的方法 getAllCards()
是在 class YgoProRepositoryImpl
中定义的,它有一个我的远程数据源的实例 class YgoProRemoteDataSource
它在这个 class 调用我的 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()
不可测试,因为我无法在我的 isolate 中注入 YgoProRemoteDataSource
的模拟 class,因为它总是从我的服务定位器获得引用.
我该怎么做才能不依赖我的服务定位器将 YgoProRemoteDataSource
注入到我的 isolate 中并使 getAllCards()
可测试?
据我所知,您有两个选择,要么通过参数注入 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() 函数作为参数传递给 class YgoProRepositoryImpl :
final Function setupLocator;
YgoProRepositoryImpl({
required this.remoteDataSource,
required this.setupLocator;
// ...
});
这样您就可以传递一个模拟来设置您的模拟 classes 或 service_locator.dart
的真实 setupLocator
。这可能不太优雅。但它应该使它可以测试,因为现在你可以模拟设置并且它没有在函数中硬编码
您真的需要测试 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
做了更认真的尝试,请看回购:https://github.com/maxim-saplin/compute_sl_test_sample
从本质上讲,Flutter/Dart 的当前状态你既不能通过闭包,也不能通过包含跨隔离边界的闭包的 类(但是当 Dart 中的新功能登陆 Flutter 时,这可能会改变 https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528).这意味着如果您不希望任何测试代码成为发布版本的一部分,您就无法通过服务定位器(其中包含闭包)或欺骗隔离以通过闭包实例化定位器的测试版本。然而,您可以轻松地将数据源实例传递到 isolate 以在其入口点用作参数。
此外,我认为要求 isolate 重建整个服务定位器没有意义。 compute() 背后的整个想法是创建一个短暂的离开隔离,运行 计算,return 结果并终止隔离。初始化定位器是一种开销,最好避免。此外,似乎 compute() 的整个概念尽可能与应用程序的其余部分隔离。
您可以克隆存储库和 运行 测试。关于示例的几句话:
- 基于 Flutter 计数器启动器应用程序
lib/classes.dart
重新创建您提供的代码片段
test/widget_test.dart
验证 YgoProRepositoryImpl
与隔离 运行 伪造的数据源版本 一起工作正常
YgoProRemoteDataSourceImpl
模拟真实实现,位于 classes.dart 和 YgoProRemoteDataSourceFake
模拟测试版本
- 运行 isolates under flutter_test 需要在
tester.runAsync()
中包装测试主体,以便实时异步执行(而不是测试默认使用假异步并依赖于抽取到进度测试时间)。 运行 这种模式下的测试可能会很慢(实际等待 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(),
);
}
我正在通过 compute()
方法使用隔离来从 API(大约 10k 个条目)中获取、解析和排序数据。
我的方法 getAllCards()
是在 class YgoProRepositoryImpl
中定义的,它有一个我的远程数据源的实例 class YgoProRemoteDataSource
它在这个 class 调用我的 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()
不可测试,因为我无法在我的 isolate 中注入 YgoProRemoteDataSource
的模拟 class,因为它总是从我的服务定位器获得引用.
我该怎么做才能不依赖我的服务定位器将 YgoProRemoteDataSource
注入到我的 isolate 中并使 getAllCards()
可测试?
据我所知,您有两个选择,要么通过参数注入 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() 函数作为参数传递给 class YgoProRepositoryImpl :
final Function setupLocator;
YgoProRepositoryImpl({
required this.remoteDataSource,
required this.setupLocator;
// ...
});
这样您就可以传递一个模拟来设置您的模拟 classes 或 service_locator.dart
的真实 setupLocator
。这可能不太优雅。但它应该使它可以测试,因为现在你可以模拟设置并且它没有在函数中硬编码
您真的需要测试 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
做了更认真的尝试,请看回购:https://github.com/maxim-saplin/compute_sl_test_sample
从本质上讲,Flutter/Dart 的当前状态你既不能通过闭包,也不能通过包含跨隔离边界的闭包的 类(但是当 Dart 中的新功能登陆 Flutter 时,这可能会改变 https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528).这意味着如果您不希望任何测试代码成为发布版本的一部分,您就无法通过服务定位器(其中包含闭包)或欺骗隔离以通过闭包实例化定位器的测试版本。然而,您可以轻松地将数据源实例传递到 isolate 以在其入口点用作参数。
此外,我认为要求 isolate 重建整个服务定位器没有意义。 compute() 背后的整个想法是创建一个短暂的离开隔离,运行 计算,return 结果并终止隔离。初始化定位器是一种开销,最好避免。此外,似乎 compute() 的整个概念尽可能与应用程序的其余部分隔离。
您可以克隆存储库和 运行 测试。关于示例的几句话:
- 基于 Flutter 计数器启动器应用程序
lib/classes.dart
重新创建您提供的代码片段test/widget_test.dart
验证YgoProRepositoryImpl
与隔离 运行 伪造的数据源版本 一起工作正常
YgoProRemoteDataSourceImpl
模拟真实实现,位于 classes.dart 和YgoProRemoteDataSourceFake
模拟测试版本- 运行 isolates under flutter_test 需要在
tester.runAsync()
中包装测试主体,以便实时异步执行(而不是测试默认使用假异步并依赖于抽取到进度测试时间)。 运行 这种模式下的测试可能会很慢(实际等待 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(),
);
}