diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 983571fa103..26cd5d4d17c 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:diff_match_patch/diff_match_patch.dart'; +import 'package:fake_async/fake_async.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test; @@ -216,39 +217,314 @@ Future testBloc, State>({ if (expect != null) { final dynamic expected = expect(); shallowEquality = '$states' == '$expected'; - try { - test.expect(states, test.wrapMatcher(expected)); - } on test.TestFailure catch (e) { - if (shallowEquality || expected is! List) rethrow; - final diff = _diff(expected: expected, actual: states); - final message = '${e.message}\n$diff'; - // ignore: only_throw_errors - throw test.TestFailure(message); - } + _validateBlocExpect(expected, states, shallowEquality); } await subscription.cancel(); await verify?.call(bloc); await tearDown?.call(); }, (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 (errors == null || !unhandledErrors.contains(error)) { - // ignore: only_throw_errors - throw error; - } + _validateBlocErrors(errors, error, unhandledErrors, shallowEquality); }, ); if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors())); } +/// Creates a new `bloc`-specific test case with the given [description]. +/// [fakeAsyncBlocTest] will handle asserting that the `bloc` emits the +/// [expect]ed states (in order) after [act] is executed. +/// +/// The main difference of [fakeAsyncBlocTest] from [blocTest] is that [setUp], +/// [act] and [tearDown] `Functions` have parameter of type [FakeAsync] which +/// provide explicitly control Dart's notion of the "current time". When the +/// time is advanced, FakeAsync fires all asynchronous events that are scheduled +/// for that time period without actually needing the test to wait for real time +/// to elapse. +/// +/// [fakeAsyncBlocTest] also handles ensuring that no additional states are +/// emitted by closing the `bloc` stream before evaluating the [expect]ation. +/// +/// [setUp] is optional and should be used to set up any dependencies prior to +/// initializing the `bloc` under test and `fakeAsync` to fire asynchronous +/// events that are scheduled for that time period. +/// [setUp] should be used to set up state necessary for a particular test case. +/// For common set up code, prefer to use `setUp` from `package:test/test.dart`. +/// +/// [build] should construct and return the `bloc` under test. +/// +/// [seed] is an optional `Function` that returns a state +/// which will be used to seed the `bloc` before [act] is called. +/// +/// [act] is an optional callback which will be invoked with the `bloc` under +/// test and should be used to interact with the `bloc` and `fakeAsync` to +/// fire asynchronous events that are scheduled for that time period. +/// +/// [skip] is an optional `int` which can be used to skip any number of states. +/// [skip] defaults to 0. +/// +/// [wait] is an optional `Duration` which can be used to wait for +/// async operations within the `bloc` under test such as `debounceTime`. +/// +/// [expect] is an optional `Function` that returns a `Matcher` which the `bloc` +/// under test is expected to emit after [act] is executed. +/// +/// [verify] is an optional callback which is invoked after [expect] +/// and can be used for additional verification/assertions. +/// [verify] is called with the `bloc` returned by [build]. +/// +/// [errors] is an optional `Function` that returns a `Matcher` which the `bloc` +/// under test is expected to throw after [act] is executed. +/// +/// [tearDown] is optional and can be used to execute any code after the +/// test has run +/// [tearDown] should be used to clean up after a particular test case. +/// For common tear down code, prefer to use `tearDown` from `package:test/test.dart`. +/// +/// [tags] is optional and if it is passed, it declares user-defined tags +/// that are applied to the test. These tags can be used to select or +/// skip the test on the command line, or to do bulk test configuration. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), +/// expect: () => [1], +/// ); +/// ``` +/// +/// [fakeAsyncBlocTest] can optionally be used with a seeded state. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'CounterBloc emits [10] when seeded with 9', +/// build: () => CounterBloc(), +/// seed: () => 9, +/// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), +/// expect: () => [10], +/// ); +/// ``` +/// +/// [fakeAsyncBlocTest] can also be used to [skip] any number of emitted states +/// before asserting against the expected states. +/// [skip] defaults to 0. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'CounterBloc emits [2] when increment is added twice', +/// build: () => CounterBloc(), +/// act: (bloc, fakeAsync) { +/// bloc +/// ..add(CounterEvent.increment) +/// ..add(CounterEvent.increment); +/// }, +/// skip: 1, +/// expect: () => [2], +/// ); +/// ``` +/// +/// [fakeAsyncBlocTest] can also be used to wait for async operations +/// by optionally providing a `Duration` to [wait]. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), +/// wait: const Duration(milliseconds: 300), +/// expect: () => [1], +/// ); +/// ``` +/// +/// [fakeAsyncBlocTest] can also be used to [verify] internal bloc +/// functionality. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), +/// expect: () => [1], +/// verify: (_) { +/// verify(() => repository.someMethod(any())).called(1); +/// } +/// ); +/// ``` +/// +/// **Note:** when using [fakeAsyncBlocTest] with state classes which don't +/// override `==` and `hashCode` you can provide an `Iterable` of matchers +/// instead of explicit state instances. +/// +/// ```dart +/// fakeAsyncBlocTest( +/// 'emits [StateB] when EventB is added', +/// build: () => MyBloc(), +/// act: (bloc, fakeAsync) => bloc.add(EventB()), +/// expect: () => [isA()], +/// ); +/// ``` +/// +/// If [tags] is passed, it declares user-defined tags that are applied to the +/// test. These tags can be used to select or skip the test on the command line, +/// or to do bulk test configuration. All tags should be declared in the +/// [package configuration file][configuring tags]. The parameter can be an +/// [Iterable] of tag names, or a [String] representing a single tag. +/// +/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags +@isTest +void fakeAsyncBlocTest, State>( + String description, { + required B Function() build, + void Function(FakeAsync async)? setUp, + State Function()? seed, + dynamic Function(B bloc, FakeAsync async)? act, + Duration? wait, + int skip = 0, + dynamic Function()? expect, + dynamic Function(B bloc)? verify, + dynamic Function()? errors, + void Function(FakeAsync async)? tearDown, + dynamic tags, +}) { + test.test( + description, + () { + testBlocFakeAsync( + setUp: setUp, + build: build, + seed: seed, + act: act, + wait: wait, + skip: skip, + expect: expect, + verify: verify, + errors: errors, + tearDown: tearDown, + ); + }, + tags: tags, + ); +} + +/// Internal [testBlocFakeAsync] runner which is only visible for testing. +/// This should never be used directly -- please use [fakeAsyncBlocTest] +/// instead. +@visibleForTesting +void testBlocFakeAsync, State>({ + required B Function() build, + void Function(FakeAsync async)? setUp, + State Function()? seed, + dynamic Function(B bloc, FakeAsync fakeAsync)? act, + Duration? wait, + int skip = 0, + dynamic Function()? expect, + dynamic Function(B bloc)? verify, + dynamic Function()? errors, + void Function(FakeAsync async)? tearDown, +}) { + var errorThrown = false; + var shallowEquality = false; + final unhandledErrors = []; + final localBlocObserver = + // ignore: deprecated_member_use + BlocOverrides.current?.blocObserver ?? Bloc.observer; + final testObserver = _TestBlocObserver( + localBlocObserver, + unhandledErrors.add, + ); + Bloc.observer = testObserver; + + fakeAsync( + (fakeAsync) => runZonedGuarded( + () { + setUp?.call(fakeAsync); + final states = []; + final bloc = build(); + // 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); + + try { + act?.call(bloc, fakeAsync); + fakeAsync.elapse(Duration.zero); + } catch (error) { + if (errors == null) rethrow; + unhandledErrors.add(error); + } + if (wait != null) fakeAsync.elapse(wait); + + fakeAsync.elapse(Duration.zero); + unawaited(bloc.close()); + + if (expect != null && !errorThrown) { + final dynamic expected = expect(); + shallowEquality = '$states' == '$expected'; + _validateBlocExpect(expected, states, shallowEquality); + } + + unawaited(subscription.cancel()); + verify?.call(bloc); + tearDown?.call(fakeAsync); + + fakeAsync.flushMicrotasks(); + }, + (Object error, _) { + try { + _validateBlocErrors( + errors, + error, + unhandledErrors, + shallowEquality, + ); + } catch (_) { + errorThrown = true; + rethrow; + } + }, + ), + ); + if (errors != null) { + test.expect(unhandledErrors, test.wrapMatcher(errors())); + } +} + +void _validateBlocErrors( + dynamic Function()? errors, + Object error, + List unhandledErrors, + bool shallowEquality, +) { + 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 (errors == null || !unhandledErrors.contains(error)) { + // ignore: only_throw_errors + throw error; + } +} + +void _validateBlocExpect( + dynamic expected, + List states, + bool shallowEquality, +) { + try { + test.expect(states, test.wrapMatcher(expected)); + } on test.TestFailure catch (e) { + if (shallowEquality || expected is! List) rethrow; + final diff = _diff(expected: expected, actual: states); + final message = '${e.message}\n$diff'; + // ignore: only_throw_errors + throw test.TestFailure(message); + } +} + class _TestBlocObserver extends BlocObserver { const _TestBlocObserver(this._localObserver, this._onError); diff --git a/packages/bloc_test/pubspec.yaml b/packages/bloc_test/pubspec.yaml index 2405c3e7957..cedb6f665ca 100644 --- a/packages/bloc_test/pubspec.yaml +++ b/packages/bloc_test/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: bloc: ^8.1.1 diff_match_patch: ^0.4.1 + fake_async: ^1.3.1 meta: ^1.3.0 mocktail: ">=0.2.0 <0.4.0" test: ^1.16.0 diff --git a/packages/bloc_test/test/bloc_fake_async_bloc_test_test.dart b/packages/bloc_test/test/bloc_fake_async_bloc_test_test.dart new file mode 100644 index 00000000000..420cf867169 --- /dev/null +++ b/packages/bloc_test/test/bloc_fake_async_bloc_test_test.dart @@ -0,0 +1,691 @@ +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', () { + group('CounterBloc', () { + fakeAsyncBlocTest( + 'supports matchers (contains)', + build: () => CounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => contains(1), + ); + + fakeAsyncBlocTest( + 'supports matchers (containsAll)', + build: () => CounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => containsAll([2, 1]), + ); + + fakeAsyncBlocTest( + 'supports matchers (containsAllInOrder)', + build: () => CounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => containsAllInOrder([1, 2]), + ); + + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => CounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => CounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + ); + + fakeAsyncBlocTest( + 'emits [1] when CounterEvent.increment is added with async act', + build: () => CounterBloc(), + act: (bloc, fakeAsync) async { + fakeAsync.elapse(const Duration(seconds: 1)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when CounterEvent.increment is called multiple times ' + 'with async act', + build: () => CounterBloc(), + act: (bloc, fakeAsync) async { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2], + ); + + fakeAsyncBlocTest( + 'emits [2] when CounterEvent.increment is added twice and skip: 1', + build: () => CounterBloc(), + act: (bloc, fakeAsync) { + bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment); + }, + skip: 1, + expect: () => const [2], + ); + + fakeAsyncBlocTest( + 'emits [11] when CounterEvent.increment is added and emitted 10', + build: () => CounterBloc(), + seed: () => 10, + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [11], + ); + + fakeAsyncBlocTest( + 'emits [11] when CounterEvent.increment is added and seed 10', + build: () => CounterBloc(), + seed: () => 10, + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [11], + ); + + test('fails immediately when expectation is incorrect', () async { + const expectedError = 'Expected: [2]\n' + ' Actual: [1]\n' + ' Which: at location [0] is <1> instead of <2>\n' + '\n' + '==== diff ========================================\n' + '\n' + '''\x1B[90m[\x1B[0m\x1B[31m[-2-]\x1B[0m\x1B[32m{+1+}\x1B[0m\x1B[90m]\x1B[0m\n''' + '\n' + '==== end diff ====================================\n' + ''; + late Object actualError; + final completer = Completer(); + await runZonedGuarded(() async { + unawaited(Future.delayed( + Duration.zero, + () => testBlocFakeAsync( + build: () => CounterBloc(), + act: (bloc, f) => bloc.add(CounterEvent.increment), + expect: () => const [2], + )).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(Future.delayed( + Duration.zero, + () => testBlocFakeAsync( + build: () => ErrorCounterBloc(), + act: (bloc, f) => bloc.add(CounterEvent.increment), + expect: () => const [1], + )).then((_) => completer.complete())); + await completer.future; + }, (Object error, _) { + actualError = error; + }); + expect(actualError, isA()); + }); + + test('fails immediately when exception occurs in act', () async { + final exception = Exception('oops'); + Object? actualError; + final completer = Completer(); + + await runZonedGuarded(() async { + unawaited(Future.delayed( + Duration.zero, + () => testBlocFakeAsync( + build: () => ErrorCounterBloc(), + act: (_, f) => throw exception, + expect: () => const [1], + )).then((_) => completer.complete())); + await completer.future; + }, (Object error, _) { + actualError = error; + if (!completer.isCompleted) completer.complete(); + }); + expect(actualError, exception); + }); + }); + + group('AsyncCounterBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => AsyncCounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => AsyncCounterBloc(), + act: (bloc, fakeAsync) { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + }, + expect: () => const [1], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when CounterEvent.increment is called multiple' + 'times with async act', + build: () => AsyncCounterBloc(), + act: (bloc, fakeAsync) { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + }, + expect: () => const [1, 2], + ); + + fakeAsyncBlocTest( + 'emits [2] when CounterEvent.increment is added twice and skip: 1', + build: () => AsyncCounterBloc(), + act: (bloc, fakeAsync) { + bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + }, + skip: 1, + expect: () => const [2], + ); + + fakeAsyncBlocTest( + 'emits [11] when CounterEvent.increment is added and emitted 10', + build: () => AsyncCounterBloc(), + seed: () => 10, + act: (bloc, fakeAsync) { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + }, + expect: () => const [11], + ); + }); + + group('DebounceCounterBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => DebounceCounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => DebounceCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + wait: const Duration(milliseconds: 300), + expect: () => const [1], + ); + + fakeAsyncBlocTest( + 'emits [2] when CounterEvent.increment ' + 'is added twice and skip: 1', + build: () => DebounceCounterBloc(), + act: (bloc, fakeAsync) async { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 305)); + bloc.add(CounterEvent.increment); + }, + skip: 1, + wait: const Duration(milliseconds: 300), + expect: () => const [2], + ); + + fakeAsyncBlocTest( + 'emits [11] when CounterEvent.increment is added and emitted 10', + build: () => DebounceCounterBloc(), + seed: () => 10, + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + wait: const Duration(milliseconds: 300), + expect: () => const [11], + ); + }); + + group('InstantEmitBloc', () { + fakeAsyncBlocTest( + 'emits [1] when nothing is added', + build: () => InstantEmitBloc(), + expect: () => const [1], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when CounterEvent.increment is added', + build: () => InstantEmitBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1, 2], + ); + + fakeAsyncBlocTest( + 'emits [1, 2, 3] when CounterEvent.increment is called' + 'multiple times with async act', + build: () => InstantEmitBloc(), + act: (bloc, fakeAsync) async { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2, 3], + ); + + fakeAsyncBlocTest( + 'emits [3] when CounterEvent.increment is added twice and skip: 2', + build: () => InstantEmitBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 2, + expect: () => const [3], + ); + + fakeAsyncBlocTest( + 'emits [11, 12] when CounterEvent.increment is added and seeded 10', + build: () => InstantEmitBloc(), + seed: () => 10, + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [11, 12], + ); + }); + + group('MultiCounterBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => MultiCounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when CounterEvent.increment is added', + build: () => MultiCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1, 2], + ); + + fakeAsyncBlocTest( + 'emits [1, 2, 3, 4] when CounterEvent.increment is called' + 'multiple times with async act', + build: () => MultiCounterBloc(), + act: (bloc, fakeAsync) { + bloc.add(CounterEvent.increment); + fakeAsync.elapse(const Duration(milliseconds: 10)); + bloc.add(CounterEvent.increment); + }, + expect: () => const [1, 2, 3, 4], + ); + + fakeAsyncBlocTest( + 'emits [4] when CounterEvent.increment is added twice and skip: 3', + build: () => MultiCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 3, + expect: () => const [4], + ); + + fakeAsyncBlocTest( + 'emits [11, 12] when CounterEvent.increment is added and emitted 10', + build: () => MultiCounterBloc(), + seed: () => 10, + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [11, 12], + ); + }); + + group('ComplexBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => ComplexBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [ComplexStateB] when ComplexEventB is added', + build: () => ComplexBloc(), + act: (bloc, fakeAsync) => bloc.add(ComplexEventB()), + expect: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] ' + 'is added and skip: 1', + build: () => ComplexBloc(), + act: (bloc, fakeAsync) => bloc + ..add(ComplexEventB()) + ..add(ComplexEventA()), + skip: 1, + expect: () => [isA()], + ); + }); + group('ErrorCounterBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => ErrorCounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [2] when increment is added twice and skip: 1', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'emits [1] when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'throws ErrorCounterBlocException when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + errors: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [1] and throws ErrorCounterBlocError ' + 'when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [] and throws ErrorCounterBlocError ' + 'when ErrorCounterBlocError thrown in act', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => throw ErrorCounterBlocError(), + expect: () => const [], + errors: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'throws two ErrorCounterBlocErrors ' + 'when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + errors: () => [ + isA(), + isA(), + ], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] and throws two ErrorCounterBlocErrors ' + 'when increment is added twice', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => [ + isA(), + isA(), + ], + ); + }); + + group('ExceptionCounterBloc', () { + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => ExceptionCounterBloc(), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [2] when increment is added twice and skip: 1', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'emits [1] when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'throws ExceptionCounterBlocException when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + errors: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [1] and throws ExceptionCounterBlocException ' + 'when increment is added', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + errors: () => [isA()], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + expect: () => const [1, 2], + errors: () => isNotEmpty, + ); + + fakeAsyncBlocTest( + 'throws two ExceptionCounterBlocExceptions ' + 'when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + errors: () => [ + isA(), + isA(), + ], + ); + + fakeAsyncBlocTest( + 'emits [1, 2] and throws two ExceptionCounterBlocException ' + 'when increment is added twice', + build: () => ExceptionCounterBloc(), + act: (bloc, fakeAsync) => 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); + }); + + fakeAsyncBlocTest( + 'emits [] when nothing is added', + build: () => SideEffectCounterBloc(repository), + expect: () => const [], + ); + + fakeAsyncBlocTest( + 'emits [1] when CounterEvent.increment is added', + build: () => SideEffectCounterBloc(repository), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + expect: () => const [1], + verify: (_) { + verify(() => repository.sideEffect()).called(1); + }, + ); + + fakeAsyncBlocTest( + 'emits [2] when CounterEvent.increment ' + 'is added twice and skip: 1', + build: () => SideEffectCounterBloc(repository), + act: (bloc, fakeAsync) => bloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment), + skip: 1, + expect: () => const [2], + ); + + fakeAsyncBlocTest( + 'does not require an expect', + build: () => SideEffectCounterBloc(repository), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + verify: (_) { + verify(() => repository.sideEffect()).called(1); + }, + ); + + fakeAsyncBlocTest( + 'async verify', + build: () => SideEffectCounterBloc(repository), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + verify: (_) async { + await Future.delayed(Duration.zero); + verify(() => repository.sideEffect()).called(1); + }, + ); + + fakeAsyncBlocTest( + 'setUp is executed before build/act', + setUp: (fakeAsync) { + when(() => repository.sideEffect()).thenThrow(Exception()); + }, + build: () => SideEffectCounterBloc(repository), + act: (bloc, fakeAsync) => 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(Future.delayed( + Duration.zero, + () => testBlocFakeAsync( + build: () => SideEffectCounterBloc(repository), + act: (bloc, f) => 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(Future.delayed( + Duration.zero, + () => testBlocFakeAsync( + build: () => ComplexBloc(), + act: (bloc, fakeAsync) => bloc.add(ComplexEventA()), + expect: () => [ComplexStateA()], + )).then((_) => completer.complete())); + await completer.future; + }, (Object error, _) { + actualError = error; + completer.complete(); + }); + expect((actualError as TestFailure).message, expectedError); + }); + }); + }); + + group('tearDown', () { + late int tearDownCallCount; + int? state; + + setUp(() { + tearDownCallCount = 0; + }); + + tearDown(() { + expect(tearDownCallCount, equals(1)); + }); + + fakeAsyncBlocTest( + 'is called after the test is run', + build: () => CounterBloc(), + act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), + verify: (bloc) { + state = bloc.state; + }, + tearDown: (fakeAsync) { + tearDownCallCount++; + expect(state, equals(1)); + }, + ); + }); +}