From e6480d0129d53f917d7037704be003e5237e9e1e Mon Sep 17 00:00:00 2001 From: Giacomo Policicchio Date: Sun, 15 Oct 2023 22:18:10 +0200 Subject: [PATCH] First Commit --- packages/bloc_test/lib/src/bloc_test.dart | 115 ++- .../test/bloc_bloc_test_saga_test.dart | 763 ++++++++++++++++++ 2 files changed, 877 insertions(+), 1 deletion(-) create mode 100644 packages/bloc_test/test/bloc_bloc_test_saga_test.dart diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 9bca3f6062b..e8ecd7157f3 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -1,10 +1,41 @@ import 'dart:async'; +import 'dart:collection'; +import 'dart:core'; import 'package:bloc/bloc.dart'; import 'package:diff_match_patch/diff_match_patch.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test; +/// Defines a step of the saga where an event is added or an action is performed +/// +/// [act] is an optional callback which will be invoked with the `bloc` under +/// test and should be used to interact with the `bloc`. In case of adding events to a bloc +/// it's simplier to use [happens], that expect an event. [act] and [happens] are mutually exclusive +/// but both are optional, a [Step] can be used to only check a state. +/// [outputs] is a list of callbacks (bool Function(S value)). For every callback a state +/// is popped out, if the callback result is true the test is passed. +/// [description] A description for the Step, it will output in message, in case of failure +/// [wait] the time to wait prior to check every output +/// [timeOut] the maximum time to wait for states output from the bloc +class Step { + Step( + {this.act, + this.happens, + required this.outputs, + this.description, + this.wait = const Duration(milliseconds: 50), + this.timeOut = const Duration(milliseconds: 150)}) { + assert(!(happens != null && act != null), "'act' and 'happens' can't be used at the sae time."); + } + final dynamic Function(B bloc)? act; + final Object? happens; + final List outputs; + final String? description; + final Duration wait; + final Duration timeOut; +} + /// Creates a new `bloc`-specific test case with the given [description]. /// [blocTest] will handle asserting that the `bloc` emits the [expect]ed /// states (in order) after [act] is executed. @@ -24,6 +55,8 @@ import 'package:test/test.dart' as test; /// [act] is an optional callback which will be invoked with the `bloc` under /// test and should be used to interact with the `bloc`. /// +/// [saga] is an optional parameter that can be used to check if a chain of events matches with desidered state changes +/// /// [skip] is an optional `int` which can be used to skip any number of states. /// [skip] defaults to 0. /// @@ -101,6 +134,33 @@ import 'package:test/test.dart' as test; /// ); /// ``` /// +/// [blocTest] can also be used to check if, given a list or event or actions, every step has the desidered output +/// by optionally providing a `Saga` to [saga]. +/// +/// ```dart +/// blocTest( +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// saga: [Step( +/// description: 'Initial', +/// outputs: [(state) => state == 0], +/// ), +/// Step( +/// happens: CounterEvent.increment, +/// description: 'Increment ', +/// outputs: [(state) => state == 1], +/// ), +/// Step( +/// act: (bloc) => bloc..add(CounterEvent.increment)..add(CounterEvent.increment), +/// description: 'Double Increment ', +/// outputs: [(state) => state == 2, +/// (state) => state == 3,], +/// timeOut: Duration(milliseconds: 200), +/// ),], +/// wait: const Duration(milliseconds: 300), +/// ); +/// ``` +/// /// [blocTest] can also be used to [verify] internal bloc functionality. /// /// ```dart @@ -142,6 +202,7 @@ void blocTest, State>( FutureOr Function()? setUp, State Function()? seed, dynamic Function(B bloc)? act, + List>? saga, Duration? wait, int skip = 0, dynamic Function()? expect, @@ -158,6 +219,7 @@ void blocTest, State>( build: build, seed: seed, act: act, + saga: saga, wait: wait, skip: skip, expect: expect, @@ -178,6 +240,7 @@ Future testBloc, State>({ FutureOr Function()? setUp, State Function()? seed, dynamic Function(B bloc)? act, + List>? saga, Duration? wait, int skip = 0, dynamic Function()? expect, @@ -201,9 +264,15 @@ Future testBloc, State>({ await setUp?.call(); final states = []; final bloc = build(); + Queue? statesQueue; // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member if (seed != null) bloc.emit(seed()); - final subscription = bloc.stream.skip(skip).listen(states.add); + final subscription = bloc.stream.skip(skip).listen((s) { + states.add(s); + if (statesQueue != null) { + statesQueue.addLast(s); + } + }); try { await act?.call(bloc); } catch (error) { @@ -212,6 +281,19 @@ Future testBloc, State>({ } if (wait != null) await Future.delayed(wait); await Future.delayed(Duration.zero); + if (saga != null && saga.isNotEmpty) { + try { + statesQueue = Queue(); + await _runSaga(bloc, saga, statesQueue); + } catch (error) { + if (errors == null) rethrow; + unhandledErrors.add(error); + } finally { + statesQueue = null; + } + } + if (wait != null) await Future.delayed(wait); + await Future.delayed(Duration.zero); await bloc.close(); if (expect != null) { final dynamic expected = expect(); @@ -247,6 +329,37 @@ Alternatively, consider using Matchers in the expect of the blocTest rather than if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors())); } +Future _runSaga(B bloc, List> saga, Queue statesQueue,) async { + for (var step in saga) { + if (step.happens != null) { + (bloc as dynamic).add(step.happens); + } + if (step.act!=null){ + await step.act?.call(bloc); + } + await Future.delayed(Duration.zero); + // await step.act.call(bloc); + int i = 0; + Stopwatch stopwatch = Stopwatch()..start(); + do { + await Future.delayed(step.wait); + if (statesQueue.isNotEmpty) { + var state = statesQueue.removeFirst(); + if (!await step.outputs[i](state)) { + var message = 'Failed check predicate [$i]'; + if (step.description != null) message = '$message - ${step.description}'; + message = '$message - State: $state'; + throw test.TestFailure(message); + } + i++; + } + } while (i < step.outputs.length && stopwatch.elapsed < step.timeOut); + if (i < step.outputs.length) { + var message = 'Failed checks : received $i states instead of ${step.outputs.length}'; + if (step.description != null) message = '$message - ${step.description}'; + } + } +} Future _runZonedGuarded(Future Function() body) { final completer = Completer(); diff --git a/packages/bloc_test/test/bloc_bloc_test_saga_test.dart b/packages/bloc_test/test/bloc_bloc_test_saga_test.dart new file mode 100644 index 00000000000..0718de9aa1e --- /dev/null +++ b/packages/bloc_test/test/bloc_bloc_test_saga_test.dart @@ -0,0 +1,763 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import 'blocs/blocs.dart'; + +class MockRepository extends Mock implements Repository {} + +void unawaited(Future? _) {} + +void main() { + group('blocTest Saga', () { + group('CounterBloc', () { + blocTest( + 'Single Step with \'act\'', + build: () => CounterBloc(), + saga: [ + Step( + act: (bloc) => bloc.add(CounterEvent.increment), + outputs: [(state) => state == 1], + ), + ], + ); + + blocTest( + 'Single Step with \'happens\'', + build: () => CounterBloc(), + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 1], + ), + ], + ); + + blocTest( + 'Mixed Steps', + build: () => CounterBloc(), + saga: [ + Step( + act: (bloc) => bloc.add(CounterEvent.increment), + outputs: [(state) => state == 1], + ), + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + ), + ], + ); + + blocTest( + 'mixed behavior', + build: () => CounterBloc(), + act: (bloc) => bloc..add(CounterEvent.increment), + saga: [ + Step( + outputs: [(state) => state == 1], + ), + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + ), + ], + ); + + blocTest( + 'containsAllInOrder with mixed behavior', + build: () => CounterBloc(), + act: (bloc) => bloc..add(CounterEvent.increment), + saga: [ + Step( + outputs: [(state) => state == 1], + ), + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + ), + ], + expect: () => containsAllInOrder([1, 2]), + ); + + blocTest( + 'emits [1, 2, 3, 4, ] when CounterEvent.increment is called multiple times ' + 'with async step', + build: () => CounterBloc(), + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 100)); + }, + outputs: [(state) => state == 3], + ), + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 4], + ), + ], + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2, 3, 4], + ); + + blocTest( + 'emits [2] when CounterEvent.increment is added twice and skip: 1', + build: () => CounterBloc(), + act: (bloc) { + bloc.add(CounterEvent.increment); + }, + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + ), + ], + skip: 1, + expect: () => const [2], + ); + + test('fails immediately when expectation is incorrect', () async { + const expectedError = + 'Failed check predicate [0] - Counter Increment Test - State: 1'; + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited( + testBloc( + build: () => CounterBloc(), + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + description: 'Counter Increment Test', + ), + ], + ).then((_) => completer.complete()), + ); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect((actualError as TestFailure).message, expectedError); + }); + + test( + 'fails immediately when ' + 'uncaught exception occurs within bloc', () async { + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited( + testBloc( + build: () => ErrorCounterBloc(), + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 1], + ), + ], + ).then((_) => completer.complete()), + ); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect(actualError, isA()); + }); + + test('fails immediately when exception occurs in act', () async { + final exception = Exception('oops'); + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited( + testBloc( + build: () => ErrorCounterBloc(), + saga: [ + Step( + act: (_) => throw exception, + outputs: [(state) => state == 1], + ), + ], + ).then((_) => completer.complete()), + ); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect(actualError, exception); + }); + + test('future still completes when uncaught exception occurs', () async { + await expectLater( + () => testBloc( + build: () => ErrorCounterBloc(), + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 2], + ), + ], + ), + throwsA(isA()), + ); + }); + }); + + group('AsyncCounterBloc', () { + blocTest( + 'emits [1] when CounterEvent.increment is added (happens)', + build: () => AsyncCounterBloc(), + saga: [ + Step( + happens: CounterEvent.increment, + outputs: [(state) => state == 1], + ), + ], + expect: () => const [1], + ); + + blocTest( + 'emits [1] when CounterEvent.increment is added (act)', + build: () => AsyncCounterBloc(), + saga: [ + Step( + act: (bloc) => bloc.add(CounterEvent.increment), + outputs: [(state) => state == 1], + ), + ], + expect: () => const [1], + ); + + blocTest( + 'emits [1, 2] when CounterEvent.increment is called multiple ' + 'times with async act', + build: () => AsyncCounterBloc(), + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + outputs: [(state) => state == 1,(state) => state == 2], + ), + ], + expect: () => const [1, 2], + ); + + blocTest( + 'emits [2] when CounterEvent.increment is added twice and skip: 1', + build: () => AsyncCounterBloc(), + saga: [ + Step( + act: (bloc) async { + bloc..add(CounterEvent.increment)..add(CounterEvent.increment); + }, + outputs: [(state) => state == 2], + ), + ], + skip: 1, + expect: () => const [2], + ); + + blocTest( + 'emits [11] when CounterEvent.increment is added and emitted 10', + build: () => AsyncCounterBloc(), + seed: () => 10, + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + }, + outputs: [(state) => state == 11], + ), + ], + expect: () => const [11], + ); + }); + + group('DebounceCounterBloc', () { + blocTest( + 'emits [] when nothing is added', + build: () => DebounceCounterBloc(), + expect: () => const [], + ); + + blocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => DebounceCounterBloc(), + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + }, + outputs: [(state) => state == 1], + ), + ], + wait: const Duration(milliseconds: 300), + expect: () => const [1], + ); + + blocTest( + 'emits [2] when CounterEvent.increment ' + 'is added twice and skip: 1', + build: () => DebounceCounterBloc(), + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 305)); + bloc.add(CounterEvent.increment); + }, + outputs: [(state) => state == 2], + ), + ], + skip: 1, + wait: const Duration(milliseconds: 300), + expect: () => const [2], + ); + + blocTest( + 'emits [11] when CounterEvent.increment is added and emitted 10', + build: () => DebounceCounterBloc(), + seed: () => 10, + saga: [ + Step( + act: (bloc) async { + bloc.add(CounterEvent.increment); + }, + outputs: [(state) => state == 11], + ), + ], + + wait: const Duration(milliseconds: 300), + expect: () => const [11], + ); + }); + + group('InstantEmitBloc', () { + blocTest( + 'emits [1] when nothing is added', + build: () => InstantEmitBloc(), + expect: () => const [1], + ); + + blocTest( + 'emits [1, 2] when CounterEvent.increment is added', + build: () => InstantEmitBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1, 2], + ); + + blocTest( + 'emits [1, 2, 3] when CounterEvent.increment is called ' + 'multiple times with async act', + build: () => InstantEmitBloc(), + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2, 3], + ); + + blocTest( + 'emits [3] when CounterEvent.increment is added twice and skip: 2', + build: () => InstantEmitBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 2, + expect: () => const [3], + ); + + blocTest( + 'emits [11, 12] when CounterEvent.increment is added and seeded 10', + build: () => InstantEmitBloc(), + seed: () => 10, + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [11, 12], + ); + }); + + group('MultiCounterBloc', () { + blocTest( + 'emits [] when nothing is added', + build: () => MultiCounterBloc(), + expect: () => const [], + ); + + blocTest( + 'emits [1, 2] when CounterEvent.increment is added', + build: () => MultiCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1, 2], + ); + + blocTest( + 'emits [1, 2, 3, 4] when CounterEvent.increment is called ' + 'multiple times with async act', + build: () => MultiCounterBloc(), + act: (bloc) async { + bloc.add(CounterEvent.increment); + await Future.delayed(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2, 3, 4], + ); + + blocTest( + 'emits [4] when CounterEvent.increment is added twice and skip: 3', + build: () => MultiCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 3, + expect: () => const [4], + ); + + blocTest( + 'emits [11, 12] when CounterEvent.increment is added and emitted 10', + build: () => MultiCounterBloc(), + seed: () => 10, + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [11, 12], + ); + }); + + group('ComplexBloc', () { + blocTest( + 'emits [] when nothing is added', + build: () => ComplexBloc(), + expect: () => const [], + ); + + blocTest( + 'emits [ComplexStateB] when ComplexEventB is added', + build: () => ComplexBloc(), + act: (bloc) => bloc.add(ComplexEventB()), + expect: () => [isA()], + ); + + blocTest( + 'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] ' + 'is added and skip: 1', + build: () => ComplexBloc(), + act: (bloc) => bloc + ..add(ComplexEventB()) + ..add(ComplexEventA()), + skip: 1, + expect: () => [isA()], + ); + }); + group('ErrorCounterBloc', () { + blocTest( + 'emits [] when nothing is added', + build: () => ErrorCounterBloc(), + expect: () => const [], + ); + + blocTest( + 'emits [2] when increment is added twice and skip: 1', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + errors: () => isNotEmpty, + ); + + blocTest( + 'emits [1] when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => isNotEmpty, + ); + + blocTest( + 'throws ErrorCounterBlocException when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + errors: () => [isA()], + ); + + blocTest( + 'emits [1] and throws ErrorCounterBlocError ' + 'when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => [isA()], + ); + + blocTest( + 'emits [1, 2] when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => isNotEmpty, + ); + + blocTest( + 'throws two ErrorCounterBlocErrors ' + 'when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + errors: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'emits [1, 2] and throws two ErrorCounterBlocErrors ' + 'when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => [ + isA(), + isA(), + ], + ); + }); + + group('ExceptionCounterBloc', () { + blocTest( + 'emits [] when nothing is added', + build: () => ExceptionCounterBloc(), + expect: () => const [], + ); + + blocTest( + 'emits [2] when increment is added twice and skip: 1', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + errors: () => isNotEmpty, + ); + + blocTest( + 'emits [1] when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => isNotEmpty, + ); + + blocTest( + 'throws ExceptionCounterBlocException when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + errors: () => [isA()], + ); + + blocTest( + 'emits [1] and throws ExceptionCounterBlocException ' + 'when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => [isA()], + ); + + blocTest( + 'emits [1, 2] when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => isNotEmpty, + ); + + blocTest( + 'throws two ExceptionCounterBlocExceptions ' + 'when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + errors: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'emits [1, 2] and throws two ExceptionCounterBlocException ' + 'when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => [ + isA(), + isA(), + ], + ); + }); + + group('SideEffectCounterBloc', () { + late Repository repository; + + setUp(() { + repository = MockRepository(); + when(() => repository.sideEffect()).thenReturn(null); + }); + + blocTest( + 'emits [] when nothing is added', + build: () => SideEffectCounterBloc(repository), + expect: () => const [], + ); + + blocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [1], + verify: (_) { + verify(() => repository.sideEffect()).called(1); + }, + ); + + blocTest( + 'emits [2] when CounterEvent.increment ' + 'is added twice and skip: 1', + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + ); + + blocTest( + 'does not require an expect', + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc.add(CounterEvent.increment), + verify: (_) { + verify(() => repository.sideEffect()).called(1); + }, + ); + + blocTest( + 'async verify', + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc.add(CounterEvent.increment), + verify: (_) async { + await Future.delayed(Duration.zero); + verify(() => repository.sideEffect()).called(1); + }, + ); + + blocTest( + 'setUp is executed before build/act', + setUp: () { + when(() => repository.sideEffect()).thenThrow(Exception()); + }, + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc.add(CounterEvent.increment), + expect: () => const [], + errors: () => [isException], + ); + + test('fails immediately when verify is incorrect', () async { + const expectedError = + '''Expected: <2>\n Actual: <1>\nUnexpected number of calls\n'''; + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited( + testBloc( + build: () => SideEffectCounterBloc(repository), + act: (bloc) => bloc.add(CounterEvent.increment), + verify: (_) { + verify(() => repository.sideEffect()).called(2); + }, + ).then((_) => completer.complete()), + ); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect((actualError as TestFailure).message, expectedError); + }); + + test('shows equality warning when strings are identical', () async { + const expectedError = ''' +Expected: [Instance of 'ComplexStateA'] + Actual: [Instance of 'ComplexStateA'] + Which: at location [0] is instead of \n +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'''; + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited( + testBloc( + build: () => ComplexBloc(), + act: (bloc) => bloc.add(ComplexEventA()), + expect: () => [ComplexStateA()], + ).then((_) => completer.complete()), + ); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect((actualError as TestFailure).message, expectedError); + }); + }); + }); + + group('tearDown', () { + late int tearDownCallCount; + int? state; + + setUp(() { + tearDownCallCount = 0; + }); + + tearDown(() { + expect(tearDownCallCount, equals(1)); + }); + + blocTest( + 'is called after the test is run', + build: () => CounterBloc(), + act: (bloc) => bloc.add(CounterEvent.increment), + verify: (bloc) { + state = bloc.state; + }, + tearDown: () { + tearDownCallCount++; + expect(state, equals(1)); + }, + ); + }); +}