felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.76k stars 3.39k forks source link

[question] how to test a bloc listener #3005

Closed aoatmon closed 2 years ago

aoatmon commented 2 years ago

use case

I would like to structure my tests in a way that allow me to

issue

afaik the typical implementation for this is a full blown widget test which may be undesirable when we don't actually need to test the widgets.

I'm trying to streamline the process, so far with mixed results

(if you want to run the code you can just clone the repo)

code ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart' as test; import 'mocks.dart'; typedef ContextTester = FutureOr Function( BuildContext context, test.WidgetTester tester, ); extension BlocProviderGenetator on Cubit { BlocProvider> get provider => BlocProvider( create: (context) => this, ); } extension BlocListProviderGenetator on Iterable { Iterable get providers sync* { for (final cubit in this) { yield cubit.provider; } } } void cubitContextTester({ Iterable listeners = const [], Iterable cubits = const [], Iterable repositories = const [], required String title, required ContextTester contextBlocTest, }) => contextTester( title: title, tester: contextBlocTest, build: (key) => MultiRepositoryProvider( providers: [EmptyRepository(), ...repositories], child: Builder( builder: (context) => MultiBlocProvider( providers: [ ...[EmptyCubit(), ...cubits].providers ], child: Builder( builder: (context) => MultiBlocListener( listeners: [EmptyCubitListener(), ...listeners], child: Builder( builder: (context) => MaterialApp(home: SizedBox(key: key)), ), ), ), ), ), ), ); void contextTester({ required String title, required Widget Function(Key) build, required ContextTester tester, }) { final key = UniqueKey(); test.testWidgets(title, (_tester) async { await _tester.pumpWidget(build(key)); final context = _tester.element(test.find.byKey(key)); await tester(context, _tester); }); } ///? source `https://github.com/felangel/bloc/blob/master/packages/bloc_test/lib/src/bloc_test.dart#L176` ///! modified ContextTester contextBlocTest, State>({ FutureOr Function(BuildContext)? setUp, required B Function(BuildContext) build, State Function(BuildContext)? seed, Function(B bloc)? act, Duration? wait, int? skip, dynamic Function(BuildContext)? expect, Function(B bloc)? verify, dynamic Function()? errors, FutureOr Function(BuildContext)? tearDown, }) { return (context, tester) async { final unhandledErrors = []; var shallowEquality = false; await runZonedGuarded( () async { await setUp?.call(context); final states = []; final bloc = build(context); // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member if (seed != null) bloc.emit(seed(context)); final subscription = bloc.stream.skip(skip ?? 0).listen(states.add); try { await act?.call(bloc); } catch (error) { unhandledErrors.add(error); } if (wait != null) await Future.delayed(wait); await Future.delayed(Duration.zero); await bloc.close(); if (expect != null) { final dynamic expected = expect(context); shallowEquality = '$states' == '$expected'; try { test.expect(states, test.wrapMatcher(expected)); } on test.TestFailure catch (e) { if (shallowEquality || expected is! List) rethrow; final message = '${e.message}\nexpected: $expected, actual: $states'; // ignore: only_throw_errors throw test.TestFailure(message); } } await subscription.cancel(); await verify?.call(bloc); await tearDown?.call(context); }, (Object error, _) { if (shallowEquality && error is test.TestFailure) { // ignore: only_throw_errors throw test.TestFailure( '''${error.message} WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n''', ); } if (!unhandledErrors.contains(error)) { // ignore: only_throw_errors throw error; } }, ); if (errors != null) { test.expect(unhandledErrors, test.wrapMatcher(errors())); } }; } ``` ```dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class EmptyCubit extends Cubit { EmptyCubit() : super(null); } class EmptyCubitListener extends BlocListener { EmptyCubitListener({Key? key}) : super( listener: (_, __) {}, key: key ?? UniqueKey(), ); } class EmptyRepository extends RepositoryProvider { EmptyRepository({Key? key}) : super( create: (_) => Null, key: key ?? UniqueKey(), ); } ``` ```dart import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'tester.dart'; /// A `CounterCubit` which manages an `int` as its state. class CounterCubit extends Cubit { /// The initial state of the `CounterCubit` is 0. CounterCubit() : super(0); /// When increment is called, the current state /// of the cubit is accessed via `state` and /// a new `state` is emitted via `emit`. void increment() => emit(state + 1); } void main() { group('CounterBloc', () { cubitContextTester( title: 'emits [] when nothing is added', cubits: [CounterCubit()], contextBlocTest: contextBlocTest( build: (context) => context.read(), expect: (context) => [], ), ); cubitContextTester( title: 'emits [1] when Increment is added', cubits: [CounterCubit()], contextBlocTest: contextBlocTest( build: (context) => context.read(), act: (bloc) => bloc.increment(), expect: (context) => [], ), ); }); } ```
pubspec ```yaml name: context_bloc_test description: looking for a way to test cubit from context publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=2.14.4 <3.0.0' flutter: ^2.5.3 dependencies: flutter: sdk: flutter flutter_bloc: ^8.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.4 bloc_test: ^9.0.1 flutter: uses-material-design: true ```
doctor ```console [✓] Flutter (Channel stable, 2.5.3, on macOS 12.0.1 21A559 darwin-x64, locale en-EE) • Flutter version 2.5.3 at /Users/francesco/fvm/versions/stable • Upstream repository https://github.com/flutter/flutter.git • Framework revision 18116933e7 (6 weeks ago), 2021-10-15 10:46:35 -0700 • Engine revision d3ea636dc5 • Dart version 2.14.4 [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) • Android SDK at /Users/francesco/Library/Android/sdk • Platform android-31, build-tools 30.0.3 • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 13.1, Build version 13A1030d • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2020.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165) [✓] VS Code (version 1.62.3) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.28.0 [✓] Connected device (2 available) • macOS (desktop) • macos • darwin-x64 • macOS 12.0.1 21A559 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 96.0.4664.55 • No issues found! ```
logs ```bash ╭─francesco@francescos-MBP ~/development/context_bloc_test ╰─$ flutter test 00:08 +0: CounterBloc emits [] when nothing is added ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following ProviderNotFoundException was thrown building _NestedHook: Error: Could not find the correct Provider above this EmptyCubitListener Widget This happens because you used a `BuildContext` that does not include the provider of your choice. There are a few common scenarios: - You added a new provider in your `main.dart` and performed a hot-reload. To fix, perform a hot-restart. - The provider you are trying to read is in a different route. Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider. - You used a `BuildContext` that is an ancestor of the provider you are trying to read. Make sure that EmptyCubitListener is under your MultiProvider/Provider. This usually happens when you are creating a provider and trying to read it immediately. For example, instead of: ``` Widget build(BuildContext context) { return Provider( create: (_) => Example(), // Will throw a ProviderNotFoundError, because `context` is associated // to the widget that is the parent of `Provider` child: Text(context.watch()), ), } ``` consider using `builder` like so: ``` Widget build(BuildContext context) { return Provider( create: (_) => Example(), // we use `builder` to obtain a new `BuildContext` that has access to the provider builder: (context) { // No longer throws return Text(context.watch()), } ), } ``` If none of these solutions work, consider asking for help on StackOverflow: https://stackoverflow.com/questions/tagged/flutter The relevant error-causing widget was: _NestedHook _NestedHook:file:///Users/francesco/.pub-cache/hosted/pub.dartlang.org/nested-1.0.0/lib/nested.dart:103:31 When the exception was thrown, this was the stack: #0 Provider._inheritedElementOf (package:provider/src/provider.dart:358:7) #1 Provider.of (package:provider/src/provider.dart:295:30) #2 ReadContext.read (package:provider/src/provider.dart:658:21) #3 _BlocListenerBaseState.initState (package:flutter_bloc/src/bloc_listener.dart:147:36) #4 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4805:57) #5 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #6 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #13 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #20 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #27 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #28 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #29 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #30 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #31 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #32 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #33 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #34 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #41 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #48 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #55 _NestedHookElement.mount (package:nested/nested.dart:187:11) #56 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #57 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #58 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #59 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #60 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #61 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #62 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #63 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #70 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #77 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #84 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #91 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #98 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #99 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #100 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #101 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #102 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #103 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #104 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #105 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #112 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #119 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #126 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) #127 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #128 Element.updateChild (package:flutter/src/widgets/framework.dart:3422:20) #129 RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1198:16) #130 RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1175:5) #131 RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1189:7) #132 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #133 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:33) #134 AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1139:19) #135 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319:5) #136 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1143:15) #137 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1080:9) #138 AutomatedTestWidgetsFlutterBinding.pump. (package:flutter_test/src/binding.dart:1006:9) #141 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41) #142 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:993:27) #143 WidgetTester.pumpWidget. (package:flutter_test/src/widget_tester.dart:554:22) #146 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41) #147 WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27) #148 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:78:19) #149 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:77:27) #150 testWidgets.. (package:flutter_test/src/widget_tester.dart:176:29) (elided 5 frames from dart:async and package:stack_trace) ════════════════════════════════════════════════════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following StateError was thrown running a test: Bad state: No element When the exception was thrown, this was the stack: #0 Iterable.single (dart:core/iterable.dart:520:25) #1 WidgetController.element (package:flutter_test/src/controller.dart:114:30) #2 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:79:29) (elided one frame from package:stack_trace) The test description was: emits [] when nothing is added ════════════════════════════════════════════════════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following message was thrown: Multiple exceptions (2) were detected during the running of the current test, and at least one was unexpected. ════════════════════════════════════════════════════════════════════════════════════════════════════ 00:08 +0 -1: CounterBloc emits [] when nothing is added [E] Test failed. See exception logs above. The test description was: emits [] when nothing is added 00:08 +0 -1: CounterBloc emits [1] when Increment is added ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following ProviderNotFoundException was thrown building _NestedHook: Error: Could not find the correct Provider above this EmptyCubitListener Widget This happens because you used a `BuildContext` that does not include the provider of your choice. There are a few common scenarios: - You added a new provider in your `main.dart` and performed a hot-reload. To fix, perform a hot-restart. - The provider you are trying to read is in a different route. Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider. - You used a `BuildContext` that is an ancestor of the provider you are trying to read. Make sure that EmptyCubitListener is under your MultiProvider/Provider. This usually happens when you are creating a provider and trying to read it immediately. For example, instead of: ``` Widget build(BuildContext context) { return Provider( create: (_) => Example(), // Will throw a ProviderNotFoundError, because `context` is associated // to the widget that is the parent of `Provider` child: Text(context.watch()), ), } ``` consider using `builder` like so: ``` Widget build(BuildContext context) { return Provider( create: (_) => Example(), // we use `builder` to obtain a new `BuildContext` that has access to the provider builder: (context) { // No longer throws return Text(context.watch()), } ), } ``` If none of these solutions work, consider asking for help on StackOverflow: https://stackoverflow.com/questions/tagged/flutter The relevant error-causing widget was: _NestedHook _NestedHook:file:///Users/francesco/.pub-cache/hosted/pub.dartlang.org/nested-1.0.0/lib/nested.dart:103:31 When the exception was thrown, this was the stack: #0 Provider._inheritedElementOf (package:provider/src/provider.dart:358:7) #1 Provider.of (package:provider/src/provider.dart:295:30) #2 ReadContext.read (package:provider/src/provider.dart:658:21) #3 _BlocListenerBaseState.initState (package:flutter_bloc/src/bloc_listener.dart:147:36) #4 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4805:57) #5 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #6 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #13 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #20 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #27 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #28 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #29 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #30 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #31 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #32 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #33 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #34 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #41 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #48 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #55 _NestedHookElement.mount (package:nested/nested.dart:187:11) #56 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #57 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #58 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #59 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #60 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #61 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #62 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #63 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #70 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #77 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #84 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #91 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #98 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #99 Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18) #100 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4690:16) #101 _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11) #102 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #103 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4643:5) #104 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4638:5) #105 _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11) ... Normal element mounting (7 frames) #112 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) ... Normal element mounting (7 frames) #119 _NestedHookElement.mount (package:nested/nested.dart:187:11) ... Normal element mounting (7 frames) #126 SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11) #127 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14) #128 Element.updateChild (package:flutter/src/widgets/framework.dart:3422:20) #129 RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1198:16) #130 RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1175:5) #131 RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1189:7) #132 Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5) #133 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:33) #134 AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1139:19) #135 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319:5) #136 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1143:15) #137 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1080:9) #138 AutomatedTestWidgetsFlutterBinding.pump. (package:flutter_test/src/binding.dart:1006:9) #141 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41) #142 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:993:27) #143 WidgetTester.pumpWidget. (package:flutter_test/src/widget_tester.dart:554:22) #146 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41) #147 WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27) #148 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:78:19) #149 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:77:27) #150 testWidgets.. (package:flutter_test/src/widget_tester.dart:176:29) (elided 5 frames from dart:async and package:stack_trace) ════════════════════════════════════════════════════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following StateError was thrown running a test: Bad state: No element When the exception was thrown, this was the stack: #0 Iterable.single (dart:core/iterable.dart:520:25) #1 WidgetController.element (package:flutter_test/src/controller.dart:114:30) #2 contextTester. (file:///Users/francesco/development/context_bloc_test/test/tester.dart:79:29) (elided one frame from package:stack_trace) The test description was: emits [1] when Increment is added ════════════════════════════════════════════════════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following message was thrown: Multiple exceptions (2) were detected during the running of the current test, and at least one was unexpected. ════════════════════════════════════════════════════════════════════════════════════════════════════ 00:08 +0 -2: CounterBloc emits [1] when Increment is added [E] Test failed. See exception logs above. The test description was: emits [1] when Increment is added 00:08 +0 -2: Some tests failed. ```

bottomline

I'd appreciate some suggestion on a better approach or if this approach makes any sense I could use some help to fix the issue

thank you

aoatmon commented 2 years ago

sorry for the wrong label :/

felangel commented 2 years ago

Hi @aoatmon 👋 Thanks for opening an issue!

I'm not sure I understand what you're trying to achieve. If you want to test your widget code (including providing blocs/cubits) then I highly recommend you use a widget test. If you want to test your blocs/cubits alone then I highly recommend you use bloc_test.

I recommend taking a look at the counter example (it includes unit, widget, and integration tests).

Hope that helps 👍

aoatmon commented 2 years ago

hi @felangel I'm not trying to test a widget, but to

  • verify the interaction between cubit and listeners
  • verify that a listener interact with context (example providers context.read<Foo>()) my current not working solution uses testWidgets
void contextTester({
  required String title,
  required Widget Function(Key) build,
  required ContextTester tester,
}) {
  final key = UniqueKey();
  test.testWidgets(title, (_tester) async {
    await _tester.pumpWidget(build(key));
    final context = _tester.element(test.find.byKey(key));
    await tester(context, _tester);
  });
}

but it fails to find widget in the context (I'm aware the logs point more at a provider issue) but my question is more open ended than "please fix my errors"

I'm trying to find a consistent way to test this aspect of the package

felangel commented 2 years ago

@aoatmon BlocListener and BlocProvider are widgets so I highly recommend using testWidgets as illustrated in the example I shared above.

To verify the interaction between cubits and listeners you can provide a mock cubit to the widget tree and use whenListen from package:bloc_test to stub the state stream.

To verify that a listener interacts with the context you can again provided a mock instance of the bloc or cubit and verify the correct API is called via verify from package:mocktail.

You can refer to the weather example tests for a complete example of what I described/recommend.

Hope that helps 👍

aoatmon commented 2 years ago

I put together this

new sample ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:bloc_route_test/src/shared/all.dart'; import '../mocks/all.dart'; void routelistenerTest, S>( String description, { required GoRouterBlocListener Function(B, GoRouter) buildListener, required B Function() buildCubit, required void Function(B) act, required String location, Duration? timeout, }) async { if (timeout != null) { Future.delayed(timeout, () => throw Exception('timeout')); } runZonedGuarded( () => testWidgets( description, (tester) async { const key = ValueKey('SizedBox'); final cubit = buildCubit(); final router = MockRouter(); when(() => router.go(location)).thenAnswer((_) { return; }); await tester.pumpWidget(MaterialApp( home: const SizedBox(key: key), builder: (context, app) => MultiBlocListener( listeners: [buildListener(cubit, router)], child: app!, ), )); final _ = tester.element(find.byKey(key)); act(cubit); await cubit.close(); verify(() => router.go(location)).called(1); }, ), (e, s) => throw Exception(e), ); } ```

it works, but I'm not sure I'm using this package as intended I would appreciate if someone could tell me if I'm doing something wrong

felangel commented 2 years ago

@aoatmon if it works for you feel free to keep using that approach but as I mentioned, I recommend following the patterns found in the examples. I can't provide much feedback because I don't feel I have enough context and there are objects like GoRouterBlocListener which are not part of the bloc library.

Closing for now but if you have any other questions that pertain to packages in the bloc library and the recommended usage, then I'm happy to continue the conversation 👍