Flutter副作用管理插件side_effect_cubit_test的使用

Flutter副作用管理插件side_effect_cubit_test的使用

本包简化了对产生副作用的Bloc进行测试的过程,它基于side_effect_cubit包,增加了允许你期望某些副作用被发出的功能。

问题:在没有side_effect_cubit_test的情况下测试副作用

传统上,在Bloc中测试副作用是一个繁琐的过程。开发者必须:

  • 手动收集副作用:创建一个列表来存储发出的副作用。
  • 订阅副作用流:建立一个StreamSubscription来监听副作用事件。
  • 将副作用添加到列表:在测试的构建步骤中,将接收到的副作用添加到之前创建的列表中。
  • 断言副作用:在验证步骤中,验证副作用列表的内容。
  • 清理:在tearDown步骤中取消StreamSubscription。
final receivedEffects = <RarOnboardingSideEffect>[];
StreamSubscription? effectSub;

blocTest<RarOnboardingBloc, RarOnboardingState>(
  "should produce RarOnboardingEkycInitFailure when init ekyc plugin fail",
  setUp: () {
    when(() => rarRepository.getFaceAuthenConfig()).thenAnswer((_) async => FaceAuthenService.defaultConfig);
    when(() => ekycPlugin.initialize(any())).thenAnswer((_) async => EKYCFailure());
  },
  build: () {
    effectSub = bloc.sideEffects.asBroadcastStream().listen(receivedEffects.add);

    return bloc;
  },
  act: (bloc) async => bloc.initConfig(),
  verify: (bloc) {
    expect(receivedEffects, [isA<RarOnboardingEkycInitFailure>()]);
  },
  tearDown: () => effectSub?.cancel()
);

这种方法引入了冗余,并使测试代码变得冗长,特别是当多个测试用例需要验证副作用时。

解决方案:side_effect_cubit_test

side_effect_cubit_test 提供了一种更简洁高效的方法来测试Bloc中的副作用。它利用现有的bloc_test包,并引入了以下关键改进:

  • 简化副作用预期expectSideEffects函数允许你在测试设置中直接指定预期的副作用。这消除了手动收集副作用、管理订阅和单独验证步骤的需求。
sideEffectBlocTest<RarOnboardingBloc, RarOnboardingState, RarOnboardingSideEffect>(
  "should produce RarOnboardingEkycInitFailure when init ekyc plugin fail",
  setUp: () {
    when(() => rarRepository.getFaceAuthenConfig()).thenAnswer((_) async => FaceAuthenService.defaultConfig);
    when(() => ekycPlugin.initialize(any())).thenAnswer((_) async => EKYCFailure());
  },
  build: () => bloc,
  act: (bloc) async => bloc.initConfig(),
  expectSideEffects: () => [isA<RarOnboardingEkycInitFailure>()],
);

完整示例代码

以下是一个完整的示例代码,展示了如何使用side_effect_cubit_test来测试带有副作用的Cubit。

import 'package:side_effect_cubit_test/side_effect_cubit_test.dart';
import 'package:side_effect_cubit/side_effect_cubit.dart';
import 'package:test/test.dart';

// 定义Cubit类
class CounterCubit extends SideEffectCubit<int, CounterSideEffect> {
  CounterCubit() : super(0);

  // 执行加一操作,并产生一个副作用
  void plusOne() {
    produceSideEffect(ShowInfoDialogEffect()); // 产生一个显示信息对话框的副作用
    emit(state + 1); // 发射新的状态
  }
}

// 定义副作用类型
abstract class CounterSideEffect {}

// 定义具体的副作用类型
class ShowInfoDialogEffect extends CounterSideEffect {}

void main() {
  final counterCubit = CounterCubit();

  // 定义测试组
  group("side_effect tests", () {
    // 使用sideEffectBlocTest进行测试
    sideEffectBlocTest<CounterCubit, int, CounterSideEffect>(
      "plusOne should yield ShowInfoDialogEffect", // 测试描述
      build: () => counterCubit, // 构建Cubit实例
      act: (cubit) => cubit.plusOne(), // 执行操作
      expect: () => [1], // 预期状态
      expectSideEffects: () => [isA<ShowInfoDialogEffect>()], // 预期副作用
    );
  });
}

更多关于Flutter副作用管理插件side_effect_cubit_test的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter副作用管理插件side_effect_cubit_test的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


side_effect_cubit_test 是一个用于管理 Flutter 应用中副作用的插件,通常与 CubitBloc 状态管理库一起使用。它允许你在状态变化时执行副作用,而不会污染你的纯状态管理逻辑。

以下是如何使用 side_effect_cubit_test 的基本步骤:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 side_effect_cubitside_effect_cubit_test 依赖:

dependencies:
  flutter:
    sdk: flutter
  side_effect_cubit: ^latest_version

dev_dependencies:
  side_effect_cubit_test: ^latest_version

然后运行 flutter pub get 来获取依赖。

2. 创建 Cubit

假设你有一个 CounterCubit,它管理一个计数器状态,并且在状态变化时触发副作用。

import 'package:side_effect_cubit/side_effect_cubit.dart';

class CounterCubit extends SideEffectCubit<int, String> {
  CounterCubit() : super(0);

  void increment() {
    emit(state + 1);
    emitSideEffect('Counter incremented to ${state + 1}');
  }

  void decrement() {
    emit(state - 1);
    emitSideEffect('Counter decremented to ${state - 1}');
  }
}

在这个例子中,CounterCubit 继承了 SideEffectCubit,它有两个泛型参数:int 表示状态类型,String 表示副作用类型。emitSideEffect 方法用于触发副作用。

3. 测试 Cubit

使用 side_effect_cubit_test 来测试 CounterCubit 的行为。

import 'package:flutter_test/flutter_test.dart';
import 'package:side_effect_cubit/side_effect_cubit.dart';
import 'package:side_effect_cubit_test/side_effect_cubit_test.dart';
import 'counter_cubit.dart';

void main() {
  group('CounterCubit', () {
    late CounterCubit counterCubit;

    setUp(() {
      counterCubit = CounterCubit();
    });

    tearDown(() {
      counterCubit.close();
    });

    test('initial state is 0', () {
      expect(counterCubit.state, 0);
    });

    sideEffectCubitTest<CounterCubit, int, String>(
      'emits side effect when increment is called',
      build: () => counterCubit,
      act: (cubit) => cubit.increment(),
      expectSideEffects: () => ['Counter incremented to 1'],
    );

    sideEffectCubitTest<CounterCubit, int, String>(
      'emits side effect when decrement is called',
      build: () => counterCubit,
      act: (cubit) => cubit.decrement(),
      expectSideEffects: () => ['Counter decremented to -1'],
    );
  });
}

在这个测试中,我们使用了 sideEffectCubitTest 来验证 CounterCubit 的状态变化和副作用。build 参数用于创建 Cubit 实例,act 参数用于执行操作,expectSideEffects 参数用于验证预期的副作用。

4. 在 UI 中使用 Cubit

在 Flutter UI 中,你可以使用 BlocBuilderBlocListener 来监听状态和副作用。

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart';

class CounterPage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: CounterView(),
    );
  }
}

class CounterView extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocListener<CounterCubit, int, String>(
        listener: (context, sideEffect) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(sideEffect)),
          );
        },
        child: Center(
          child: BlocBuilder<CounterCubit, int>(
            builder: (context, state) {
              return Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Count: $state'),
                  SizedBox(height: 20),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ElevatedButton(
                        onPressed: () => context.read<CounterCubit>().increment(),
                        child: Text('Increment'),
                      ),
                      SizedBox(width: 20),
                      ElevatedButton(
                        onPressed: () => context.read<CounterCubit>().decrement(),
                        child: Text('Decrement'),
                      ),
                    ],
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}
回到顶部