Flutter状态管理插件getbloc_test的使用

Flutter状态管理插件getbloc_test的使用

单元测试与testController

testController 创建一个新的特定于 controller 的测试用例,通过给定的 descriptiontestController 将处理断言 controller 在执行 act 后是否按顺序发出 expect 预期的状态。testController 还会确保在评估 expect 之前关闭 controller 流以防止额外的状态被发出。

  • build 应该用于所有 controller 的初始化和准备,并且必须返回正在测试的 controller
  • seed 是一个可选函数,它返回一个状态,在调用 act 之前用来填充 controller
  • act 是一个可选回调,它将在测试用例中被调用,并且应该用于与 controller 交互。
  • skip 是一个可选整数,可以用来跳过任意数量的状态。默认值为 0。
  • wait 是一个可选的 Duration,可以在测试 controller 中的异步操作时使用,例如 debounceTime
  • expect 是一个可选函数,它返回一个 Matcher,用于测试 controller 是否在 act 执行后发出预期的状态。
  • verify 是一个可选回调,可以在 expect 之后调用,用于进行额外的验证/断言。verify 调用时会传入由 build 返回的 controller
  • errors 是一个可选函数,它返回一个 Matcher,用于测试 controlleract 执行后是否抛出预期的异常。

示例代码

group('CounterController', () {
  testController(
    'emits [] when nothing is added',
    build: () => CounterController(),
    expect: () => [],
  );

  testController(
    'emits [1] when CounterEvent.increment is added',
    build: () => CounterController(),
    act: (controller) => controller.add(CounterEvent.increment),
    expect: () => [1],
  );
});

带有种子状态的示例

testController(
  'CounterController emits [10] when seeded with 9',
  build: () => CounterController(),
  seed: () => 9,
  act: (controller) => controller.increment(),
  expect: () => [10],
);

跳过状态的示例

testController(
  'CounterController emits [2] when CounterEvent.increment is added twice',
  build: () => CounterController(),
  act: (controller) => controller..add(CounterEvent.increment)..add(CounterEvent.increment),
  skip: 1,
  expect: () => [2],
);

等待异步操作的示例

testController(
  'CounterController emits [1] when CounterEvent.increment is added',
  build: () => CounterController(),
  act: (controller) => controller.add(CounterEvent.increment),
  wait: const Duration(milliseconds: 300),
  expect: () => [1],
);

验证内部控制器功能的示例

testController(
  'CounterController emits [1] when CounterEvent.increment is added',
  build: () => CounterController(),
  act: (controller) => controller.add(CounterEvent.increment),
  expect: () => [1],
  verify: (_) {
    verify(() => repository.someMethod(any())).called(1);
  }
);

期望异常的示例

testController(
  'CounterController throws Exception when null is added',
  build: () => CounterController(),
  act: (controller) => controller.add(null),
  errors: () => [isA<Exception>()]
);

注意事项

当使用 testController 与未重写 ==hashCode 的状态类时,您可以提供一个匹配器的 Iterable 而不是显式的状态实例。

testController(
  'emits [StateB] when MyEvent is added',
  build: () => MyController(),
  act: (controller) => controller.add(MyEvent()),
  expect: () => [isA<StateB>()],
);

完整示例Demo

以下是一个完整的示例,展示了如何使用 getbloc_test 插件来测试一个简单的计数器控制器。

项目结构

lib/
  main.dart
  counter_page.dart
  counter_controller.dart
  counter_event.dart
  counter_binding.dart

main.dart

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:getbloc/getbloc.dart';
import 'package:getbloc_test/getbloc_test.dart';

/// A Mocked Counter Controller
class MockCounterController extends MockController<CounterEvent, int>
    implements CounterController {}

/// A Stub of Counter Event
class FakeCounterEvent extends Fake implements CounterEvent {}

void main() {
  late CounterController counterController;

  setUpAll(() {
    registerFallbackValue<CounterEvent>(FakeCounterEvent());
  });

  setUp(() {
    counterController = Get.put(MockCounterController());
  });

  tearDown(Get.reset);

  testWidgets('renders a counter 5', (WidgetTester tester) async {
    counterController.whenState(5);
    await tester.pumpWidget(GetMaterialApp(home: CounterPage()));
    expect(find.text('5'), findsOneWidget);
  });

  testWidgets('renders a counter 10 and call increment event',
      (WidgetTester tester) async {
    counterController.whenState(10);
    await tester.pumpWidget(GetMaterialApp(home: CounterPage()));
    await tester.tap(find.byTooltip('increment'));
    expect(find.text('10'), findsOneWidget);
    verify(() => counterController.add(Increment())).called(1);
  });

  testWidgets('renders a counter -1 and call decrement event',
      (WidgetTester tester) async {
    counterController.whenState(-1);
    await tester.pumpWidget(GetMaterialApp(home: CounterPage()));
    await tester.tap(find.byTooltip('decrement'));
    expect(find.text('-1'), findsOneWidget);
    verify(() => counterController.add(Decrement())).called(1);
  });
}

counter_page.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'counter_controller.dart';

/// A [StatelessWidget] which uses:
/// * [get](https://pub.dev/packages/get)
/// * [getbloc](https://pub.dev/packages/getbloc)
/// to manage the state of a counter.
class App extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return GetMaterialApp(
      getPages: [
        GetPage<CounterPage>(
          name: '/',
          page: () => CounterPage(),
          binding: CounterBinding(),
        ),
      ],
    );
  }
}

/// Binding class to connect the page with the controller
class CounterBinding extends Bindings {
  [@override](/user/override)
  void dependencies() {
    Get.lazyPut(() => CounterController());
  }
}

/// A [StatelessWidget] which demonstrates
/// how to consume and interact with a [CounterController].
class CounterPage extends GetView<CounterController> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: controller.obx(
        (counter) => Text(
          '$counter',
          style: Theme.of(context).textTheme.headline1,
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: _buttons,
      ),
    );
  }

  List<Widget> get _buttons => [
        _getButton(
          Icons.add,
          () => controller.add(Increment()),
          'increment',
        ),
        _getButton(
          Icons.remove,
          () => controller.add(Decrement()),
          'decrement',
        ),
      ];

  Widget _getButton(
          IconData iconData, VoidCallback onPressed, String tooltipText) =>
      Padding(
        padding: const EdgeInsets.all(5.0),
        child: FloatingActionButton(
          tooltip: tooltipText,
          backgroundColor: Get.theme.floatingActionButtonTheme.backgroundColor,
          child: Icon(iconData),
          onPressed: onPressed,
        ),
      );
}

counter_controller.dart

import 'package:get/get.dart';
import 'counter_event.dart';

/// {[@template](/user/template) counter_controller}
/// A simple [Controller] which manages an `int` as its state.
/// {[@endtemplate](/user/endtemplate)}
class CounterController extends Controller<CounterEvent, int> {
  /// {[@macro](/user/macro) counter_controller}
  CounterController() : super(0);

  [@override](/user/override)
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event is Increment) {
      yield state + 1;
    }
    if (event is Decrement) {
      yield state - 1;
    }
  }
}

counter_event.dart

import 'package:equatable/equatable.dart';

/// {[@template](/user/template) counter_event}
/// Event being processed by [CounterController].
/// {[@endtemplate](/user/endtemplate)}
abstract class CounterEvent extends Equatable {
  /// {[@macro](/user/macro) counter_event}
  const CounterEvent();

  [@override](/user/override)
  List<Object> get props => [];
}

/// Notifies controller to increment state.
class Increment extends CounterEvent {}

/// Notifies controller to decrement state.
class Decrement extends CounterEvent {}

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

1 回复

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


当然,以下是一个关于如何在Flutter项目中使用get_itbloc进行状态管理的示例代码。虽然你提到的是getbloc_test,但通常我们结合使用get_it(一个依赖注入容器)和bloc(一个状态管理库)。这里我们假设你想了解如何使用这两个库来管理Flutter应用的状态。

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.0
  get_it: ^7.2.0

dev_dependencies:
  bloc_test: ^8.0.0
  flutter_test:
    sdk: flutter

2. 配置get_it

创建一个get_it_instance.dart文件来配置你的GetIt容器:

import 'package:get_it/get_it.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// 定义GetIt实例
final GetIt getIt = GetIt.instance;

void setupLocator() {
  // 注册BlocDelegate,用于调试和日志记录(可选)
  getIt.registerSingleton<BlocDelegate>(BlocSupervisor.delegate);

  // 注册你的Bloc实例(示例)
  getIt.registerFactory<CounterBloc>(() => CounterBloc(counterEventSink: getIt<CounterEventSink>()));
  getIt.registerFactory<CounterEventSink>(() => CounterEventSink());
}

// 示例事件类
class IncrementEvent {}

// 示例事件接收器(用于依赖注入)
class CounterEventSink {
  final _controllers = <VoidCallback>[];

  void addListener(VoidCallback listener) {
    _controllers.add(listener);
  }

  void dispatchIncrement() {
    for (final listener in _controllers) {
      listener();
    }
  }
}

3. 创建Bloc

创建一个counter_bloc.dart文件来定义你的Bloc:

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'get_it_instance.dart';

part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  final Function() counterEventSink;

  CounterBloc({required this.counterEventSink}) : super(CounterInitial());

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if (event is IncrementEvent) {
      counterEventSink(); // 触发事件接收器
      yield CounterIncremented(count: state.count + 1);
    }
  }
}

4. 定义事件和状态

counter_bloc.dart文件中定义事件和状态:

// counter_event.dart
part of 'counter_bloc.dart';

abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

// counter_state.dart
part of 'counter_bloc.dart';

abstract class CounterState {}

class CounterInitial extends CounterState {
  final int count = 0;
}

class CounterIncremented extends CounterState {
  final int count;

  CounterIncremented({required this.count});
}

5. 使用BlocProvider和BlocBuilder

在你的主文件(如main.dart)中使用BlocProviderBlocBuilder

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'get_it_instance.dart';
import 'counter_bloc.dart';

void main() {
  setupLocator();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Flutter Bloc Example')),
        body: BlocProvider<CounterBloc>(
          create: (context) => getIt<CounterBloc>(),
          child: MyHomePage(),
        ),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          if (state is CounterInitial || state is CounterIncremented) {
            return Text('You have pushed the button this many times: ${state.count}');
          }
          return Text('Unknown state');
        },
      ),
    );
  }
}

6. 触发事件

在你的UI中触发Bloc事件,例如在按钮点击时:

class MyHomePage extends StatelessWidget {
  final CounterEventSink counterEventSink = getIt<CounterEventSink>();

  @override
  Widget build(BuildContext context) {
    counterEventSink.addListener(() {
      context.read<CounterBloc>().add(IncrementEvent());
    });

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          BlocBuilder<CounterBloc, CounterState>(
            builder: (context, state) {
              if (state is CounterInitial || state is CounterIncremented) {
                return Text('You have pushed the button this many times: ${state.count}');
              }
              return Text('Unknown state');
            },
          ),
          SizedBox(height: 20),
          ElevatedButton(
            onPressed: () {
              getIt<CounterEventSink>().dispatchIncrement();
            },
            child: Text('Increment'),
          ),
        ],
      ),
    );
  }
}

注意:在上面的代码中,我们通过getIt<CounterEventSink>().dispatchIncrement()来触发事件,这实际上会调用addListener中注册的回调,从而触发Bloc的状态更新。

这个例子展示了如何使用get_itbloc进行状态管理,并结合了事件监听和依赖注入。当然,你可以根据实际需求进一步扩展和优化这个示例。

回到顶部