【问题标题】:Use a specific instance of a class inside an isolate在隔离内使用类的特定实例
【发布时间】:2022-01-01 07:23:15
【问题描述】:

我通过compute() 方法使用隔离来从 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() 可测试?

【问题讨论】:

    标签: flutter dart service-locator


    【解决方案1】:

    做了更认真的尝试,请看repo:https://github.com/maxim-saplin/compute_sl_test_sample

    基本上就 Flutter/Dart 的当前状态而言,您既不能传递闭包,也不能跨隔离边界传递包含闭包的类(然而,当 Dart 中的新功能登陆 Flutter https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528 时,这可能会改变)。这意味着如果您不希望任何测试代码成为发布版本的一部分,则您无法传递服务定位器(包含闭包)或欺骗隔离以通过闭包实例化定位器的测试版本。然而,您可以轻松地将数据源实例传递给隔离,以便在其入口点作为参数使用。

    此外,我认为要求隔离来重建整个服务定位器是没有意义的。 compute() 背后的整个想法是创建一个短的离开隔离,运行计算,返回结果并终止隔离。初始化定位器是一种开销,最好避免。此外,compute() 的整个概念似乎与应用程序的其余部分尽可能隔离。

    您可以克隆存储库并运行测试。关于样本的几句话:

    • 基于 Flutter 计数器入门应用
    • lib/classes.dart 重新创建您提供的代码 sn-p
    • test/widget_test.dart 验证 YgoProRepositoryImpl 在隔离运行假数据源版本时工作正常
    • YgoProRemoteDataSourceImpl 模仿真实实现,位于 classes.dart 和 YgoProRemoteDataSourceFake 模仿测试版本
    • 在 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(),
      );
    }
    

    【讨论】:

    • 非常感谢您的详细回答。我无法应用您示例中的原则,因为我的项目的问题是我通过getCardInfo 在隔离内部执行 GET 请求,这是我隔离失败的原因。最后,我决定将我的调用直接移至方法getCardInfo 内的compute,以仅管理对我的响应的解析。我仍然会验证你的回答,因为它帮助我理解了我的实施中出了什么问题。
    • 另外,为了确保您的答案不会过多地依赖于您的 repo 的链接,您是否可以编辑它以添加一些代码 sn-ps。
    • 很高兴它有帮助!添加代码sn-ps
    【解决方案2】:

    您真的需要测试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
    
    

    【讨论】:

    • 好点,从单元测试的角度来看,将 compute() 调用保持在测试范围之外是有意义的。然而,从集成测试的角度来看,尝试测试特定代码在生成的隔离中的行为是有意义的,坦率地说,这很棘手。例如。生成的隔离可能会错过 Flutter 初始化,例如调用 ensureInitialized() 此外,Dart 隔离中可能存在重大更改和错误;) Flutter 2.5.0 带来了这样的变化:github.com/dart-lang/sdk/issues/46754 OR github.com/dart-lang/sdk/issues/47672
    • 基本上我的测试是检查方法getCardInfo 是否被正确调用并确保getAllCards 返回一个YgoCard 的排序列表。所以是的,理论上我不需要直接测试_fetchCards()setupLocator(),但我需要能够伪造getCardInfo 返回的数据,我通常通过模拟YgoProRemoteDataSource 类但我的定位器无法注入模拟类,所以我无法为我的测试返回一致的数据。您能否通过“可测试的静态功能并预先设置您的定位器”详细说明您的想法?
    • 更新了一个例子
    【解决方案3】:

    据我了解,您有两个选择,要么通过参数注入 static Future&lt;List&lt;YgoCard&gt;&gt; _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;
      }
    
    

    编辑

    刚刚更新了答案,因为在这里编辑它比在 cmets 中更容易...

    嗯,我能想到的唯一解决方法是将 setupLocator() 函数作为参数传递给 YgoProRepositoryImpl 类:

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

    这样你可以传递一个模拟来设置你的模拟类或你的service_locator.dart 的真实setupLocator。这可能不太优雅。但它应该使它可测试,因为现在你可以模拟设置并且它不是在函数中硬编码

    【讨论】:

    • 已经尝试过了,我得到了Invalid argument(s): Illegal argument in isolate message 异常。并且直接在服务定位器中注入我的模拟类会使其依赖于生成的类,这些类应该只公开用于单元测试。
    • 这是因为你不能在一个隔离内部传递任何类,支持的类在SendPort.send的文档中定义:api.flutter.dev/flutter/dart-isolate/SendPort/send.html
    • 感谢您的反应,但不幸的是您的编辑也不起作用,您不能将 Function 作为参数传递给隔离,如我链接的文档中所述。
    • 是的,我不建议将它传递给隔离/计算。但是到该方法所在的类。因此,不要从 service_locator.dart 调用 setupLocator,而是调用通过 YgoProRepositoryImpl 构造函数注入的那个。
    • 是的,我明白这一点,但我需要在我的隔离内部调用 setupLocator,但由于隔离执行的方法需要是静态或顶级函数,我必须传递引用在某些时候将 setupLocator 作为参数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-24
    • 1970-01-01
    • 1970-01-01
    • 2016-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多