From a90d51db3c04807cf9c61238dfbed009db209b46 Mon Sep 17 00:00:00 2001 From: sergepankov Date: Tue, 18 Apr 2023 23:41:23 +0100 Subject: [PATCH 1/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. introduced fakeAsyncBlocTest 2. fakeAsyncBlocTest tests -> 100% coverage 3. added fake_async dependency --- packages/bloc_test/lib/src/bloc_test.dart | 265 +++++++ packages/bloc_test/pubspec.yaml | 1 + .../test/bloc_fake_async_bloc_test_test.dart | 691 ++++++++++++++++++ packages/bloc_test/test/sandbox.dart | 14 + 4 files changed, 971 insertions(+) create mode 100644 packages/bloc_test/test/bloc_fake_async_bloc_test_test.dart create mode 100644 packages/bloc_test/test/sandbox.dart diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index a90fb06dfbf..da3af9c887a 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; @@ -170,6 +171,270 @@ void blocTest, State>( ); } +/// 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' has 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. +/// [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`. +/// +/// [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 +/// blocTest( +/// '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 +/// blocTest( +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc) => bloc.add(CounterEvent.increment), +/// wait: const Duration(milliseconds: 300), +/// expect: () => [1], +/// ); +/// ``` +/// +/// [fakeAsyncBlocTest] can also be used to [verify] internal bloc functionality. +/// +/// ```dart +/// blocTest( +/// '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 +/// blocTest( +/// '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, { + void Function(FakeAsync async)? setUp, + required B Function() build, + State Function()? seed, + Function(B bloc, FakeAsync async)? act, + Duration? wait, + int skip = 0, + dynamic Function()? expect, + 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>({ + void Function(FakeAsync async)? setUp, + required B Function() build, + State Function()? seed, + Function(B bloc, FakeAsync fakeAsync)? act, + Duration? wait, + int skip = 0, + dynamic Function()? expect, + 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'; + 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); + } + } + + unawaited(subscription.cancel()); + verify?.call(bloc); + tearDown?.call(fakeAsync); + + fakeAsync.flushMicrotasks(); + }, + (Object error, _) { + if (shallowEquality && error is test.TestFailure) { + errorThrown = true; + // 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)) { + errorThrown = true; + // ignore: only_throw_errors + throw error; + } + }, + )); + if (errors != null) { + test.expect(unhandledErrors, test.wrapMatcher(errors())); + } +} + /// Internal [blocTest] runner which is only visible for testing. /// This should never be used directly -- please use [blocTest] instead. @visibleForTesting diff --git a/packages/bloc_test/pubspec.yaml b/packages/bloc_test/pubspec.yaml index d2ba66d8cc9..0c9e98a424d 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)); + }, + ); + }); +} diff --git a/packages/bloc_test/test/sandbox.dart b/packages/bloc_test/test/sandbox.dart new file mode 100644 index 00000000000..263a719360e --- /dev/null +++ b/packages/bloc_test/test/sandbox.dart @@ -0,0 +1,14 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:test/test.dart'; + +import 'blocs/blocs.dart'; + +void main() { + fakeAsyncBlocTest( + 'emits [1] and throws ErrorCounterBlocError ' + 'when increment is added', + build: () => ErrorCounterBloc(), + act: (bloc, fakeAsync) => throw ErrorCounterBlocError(), + errors: () => [isA()], + ); +} From d0fc9a6085e7b3973d4c269456db34e91bdd8e8c Mon Sep 17 00:00:00 2001 From: sergepankov Date: Tue, 18 Apr 2023 23:46:25 +0100 Subject: [PATCH 2/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Removed sandbox file --- packages/bloc_test/test/sandbox.dart | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 packages/bloc_test/test/sandbox.dart diff --git a/packages/bloc_test/test/sandbox.dart b/packages/bloc_test/test/sandbox.dart deleted file mode 100644 index 263a719360e..00000000000 --- a/packages/bloc_test/test/sandbox.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; - -void main() { - fakeAsyncBlocTest( - 'emits [1] and throws ErrorCounterBlocError ' - 'when increment is added', - build: () => ErrorCounterBloc(), - act: (bloc, fakeAsync) => throw ErrorCounterBlocError(), - errors: () => [isA()], - ); -} From f5af29f756cec3f56a3c0971f471dfeddd2e7154 Mon Sep 17 00:00:00 2001 From: sergepankov Date: Wed, 19 Apr 2023 12:09:45 +0100 Subject: [PATCH 3/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Fixed linter warnings -> Avoid lines longer than 80 characters. --- packages/bloc_test/lib/src/bloc_test.dart | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index da3af9c887a..29a47551f77 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -172,18 +172,18 @@ void blocTest, State>( } /// 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. +/// [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' has 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. +/// [act] and [tearDown] 'Functions' has 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. +/// [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. @@ -275,7 +275,8 @@ void blocTest, State>( /// ); /// ``` /// -/// [fakeAsyncBlocTest] can also be used to [verify] internal bloc functionality. +/// [fakeAsyncBlocTest] can also be used to [verify] internal bloc +/// functionality. /// /// ```dart /// blocTest( @@ -289,9 +290,9 @@ void blocTest, State>( /// ); /// ``` /// -/// **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. +/// **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 /// blocTest( From e71865a3cf920070fdd2612ff0bd26d3c73c7fdf Mon Sep 17 00:00:00 2001 From: sergepankov Date: Wed, 19 Apr 2023 17:23:24 +0100 Subject: [PATCH 4/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Modified doc comments & fixed typo's --- packages/bloc_test/lib/src/bloc_test.dart | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 29a47551f77..925b1c11fba 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -176,7 +176,7 @@ void blocTest, State>( /// [expect]ed states (in order) after [act] is executed. /// /// The main difference of [fakeAsyncBlocTest] from [blocTest] is that [setUp], -/// [act] and [tearDown] 'Functions' has parameter of type [FakeAsync] which +/// [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 @@ -185,8 +185,9 @@ void blocTest, State>( /// [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. +/// [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`. /// @@ -196,7 +197,8 @@ void blocTest, 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`. +/// 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. @@ -214,8 +216,8 @@ void blocTest, State>( /// [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] 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`. /// @@ -249,7 +251,7 @@ void blocTest, State>( /// [skip] defaults to 0. /// /// ```dart -/// blocTest( +/// fakeAsyncBlocTest( /// 'CounterBloc emits [2] when increment is added twice', /// build: () => CounterBloc(), /// act: (bloc, fakeAsync) { @@ -266,10 +268,10 @@ void blocTest, State>( /// by optionally providing a `Duration` to [wait]. /// /// ```dart -/// blocTest( +/// fakeAsyncBlocTest( /// 'CounterBloc emits [1] when increment is added', /// build: () => CounterBloc(), -/// act: (bloc) => bloc.add(CounterEvent.increment), +/// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), /// wait: const Duration(milliseconds: 300), /// expect: () => [1], /// ); @@ -279,7 +281,7 @@ void blocTest, State>( /// functionality. /// /// ```dart -/// blocTest( +/// fakeAsyncBlocTest( /// 'CounterBloc emits [1] when increment is added', /// build: () => CounterBloc(), /// act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment), @@ -295,7 +297,7 @@ void blocTest, State>( /// instead of explicit state instances. /// /// ```dart -/// blocTest( +/// fakeAsyncBlocTest( /// 'emits [StateB] when EventB is added', /// build: () => MyBloc(), /// act: (bloc, fakeAsync) => bloc.add(EventB()), From 8b7fbc910070b8fe87db8db82362c1fcec8f181f Mon Sep 17 00:00:00 2001 From: sergepankov Date: Wed, 19 Apr 2023 17:31:50 +0100 Subject: [PATCH 5/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Replaced ' with ` in doc comments --- packages/bloc_test/lib/src/bloc_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 925b1c11fba..49395dc053d 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -176,7 +176,7 @@ void blocTest, State>( /// [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 +/// [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 @@ -186,7 +186,7 @@ void blocTest, State>( /// 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 +/// 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`. @@ -197,7 +197,7 @@ void blocTest, 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 +/// 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. From 5a4a9f653b9e6f950ba558628f8ad9eff4220b70 Mon Sep 17 00:00:00 2001 From: sergepankov Date: Thu, 20 Apr 2023 02:36:44 +0100 Subject: [PATCH 6/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Extracted duplicated code --- packages/bloc_test/lib/src/bloc_test.dart | 185 +++++++++++----------- 1 file changed, 90 insertions(+), 95 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 49395dc053d..ff336806f97 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -171,6 +171,65 @@ void blocTest, State>( ); } +/// Internal [blocTest] runner which is only visible for testing. +/// This should never be used directly -- please use [blocTest] instead. +@visibleForTesting +Future testBloc, State>({ + FutureOr Function()? setUp, + required B Function() build, + State Function()? seed, + Function(B bloc)? act, + Duration? wait, + int skip = 0, + dynamic Function()? expect, + Function(B bloc)? verify, + dynamic Function()? errors, + FutureOr Function()? tearDown, +}) async { + 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; + + await runZonedGuarded( + () async { + await setUp?.call(); + 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 { + await act?.call(bloc); + } catch (error) { + if (errors == null) rethrow; + 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(); + shallowEquality = '$states' == '$expected'; + _validateBlocExpect(expected, states, shallowEquality); + } + await subscription.cancel(); + await verify?.call(bloc); + await tearDown?.call(); + }, + (Object 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. @@ -399,15 +458,7 @@ void testBlocFakeAsync, State>({ if (expect != null && !errorThrown) { 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); } unawaited(subscription.cancel()); @@ -417,19 +468,12 @@ void testBlocFakeAsync, State>({ fakeAsync.flushMicrotasks(); }, (Object error, _) { - if (shallowEquality && error is test.TestFailure) { - errorThrown = true; - // 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)) { + try { + _validateBlocErrors( + errors, error, unhandledErrors, shallowEquality); + } catch (_) { errorThrown = true; - // ignore: only_throw_errors - throw error; + rethrow; } }, )); @@ -438,82 +482,33 @@ Alternatively, consider using Matchers in the expect of the blocTest rather than } } -/// Internal [blocTest] runner which is only visible for testing. -/// This should never be used directly -- please use [blocTest] instead. -@visibleForTesting -Future testBloc, State>({ - FutureOr Function()? setUp, - required B Function() build, - State Function()? seed, - Function(B bloc)? act, - Duration? wait, - int skip = 0, - dynamic Function()? expect, - Function(B bloc)? verify, - dynamic Function()? errors, - FutureOr Function()? tearDown, -}) async { - 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; - - await runZonedGuarded( - () async { - await setUp?.call(); - 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 { - await act?.call(bloc); - } catch (error) { - if (errors == null) rethrow; - 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(); - 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); - } - } - 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} +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; - } - }, - ); - if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors())); + ); + } + 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 { From 4ee0f866e2bc99964613508aa2b14a76cc26251f Mon Sep 17 00:00:00 2001 From: sergepankov Date: Sat, 27 May 2023 23:44:16 +0100 Subject: [PATCH 7/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Fixed formatting in bloc_test --- packages/bloc_test/lib/src/bloc_test.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index ff336806f97..fe86b85f789 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -470,7 +470,11 @@ void testBlocFakeAsync, State>({ (Object error, _) { try { _validateBlocErrors( - errors, error, unhandledErrors, shallowEquality); + errors, + error, + unhandledErrors, + shallowEquality, + ); } catch (_) { errorThrown = true; rethrow; @@ -482,8 +486,12 @@ void testBlocFakeAsync, State>({ } } -void _validateBlocErrors(dynamic Function()? errors, Object error, - List unhandledErrors, bool shallowEquality) { +void _validateBlocErrors( + dynamic Function()? errors, + Object error, + List unhandledErrors, + bool shallowEquality, +) { if (shallowEquality && error is test.TestFailure) { // ignore: only_throw_errors throw test.TestFailure( @@ -499,7 +507,10 @@ Alternatively, consider using Matchers in the expect of the blocTest rather than } void _validateBlocExpect( - dynamic expected, List states, bool shallowEquality) { + dynamic expected, + List states, + bool shallowEquality, +) { try { test.expect(states, test.wrapMatcher(expected)); } on test.TestFailure catch (e) { From c73593f7c220b24ac0c207c092e9f343f0b5f2fb Mon Sep 17 00:00:00 2001 From: sergepankov Date: Sat, 27 May 2023 23:58:08 +0100 Subject: [PATCH 8/8] feat(bloc_test): FakeAsync support - fire all asynchronous events without actually needing to wait for real time to elapse 1. Sync with master & conflicts resolve --- packages/bloc_test/lib/src/bloc_test.dart | 101 +++++++++++----------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index b2005d4c2bd..26cd5d4d17c 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -374,14 +374,14 @@ Future testBloc, State>({ @isTest void fakeAsyncBlocTest, State>( String description, { - void Function(FakeAsync async)? setUp, required B Function() build, + void Function(FakeAsync async)? setUp, State Function()? seed, - Function(B bloc, FakeAsync async)? act, + dynamic Function(B bloc, FakeAsync async)? act, Duration? wait, int skip = 0, dynamic Function()? expect, - Function(B bloc)? verify, + dynamic Function(B bloc)? verify, dynamic Function()? errors, void Function(FakeAsync async)? tearDown, dynamic tags, @@ -411,14 +411,14 @@ void fakeAsyncBlocTest, State>( /// instead. @visibleForTesting void testBlocFakeAsync, State>({ - void Function(FakeAsync async)? setUp, required B Function() build, + void Function(FakeAsync async)? setUp, State Function()? seed, - Function(B bloc, FakeAsync fakeAsync)? act, + dynamic Function(B bloc, FakeAsync fakeAsync)? act, Duration? wait, int skip = 0, dynamic Function()? expect, - Function(B bloc)? verify, + dynamic Function(B bloc)? verify, dynamic Function()? errors, void Function(FakeAsync async)? tearDown, }) { @@ -434,53 +434,55 @@ void testBlocFakeAsync, State>({ ); 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( + (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); - unawaited(bloc.close()); + } catch (error) { + if (errors == null) rethrow; + unhandledErrors.add(error); + } + if (wait != null) fakeAsync.elapse(wait); - if (expect != null && !errorThrown) { - final dynamic expected = expect(); - shallowEquality = '$states' == '$expected'; - _validateBlocExpect(expected, states, shallowEquality); - } + fakeAsync.elapse(Duration.zero); + unawaited(bloc.close()); - unawaited(subscription.cancel()); - verify?.call(bloc); - tearDown?.call(fakeAsync); + if (expect != null && !errorThrown) { + final dynamic expected = expect(); + shallowEquality = '$states' == '$expected'; + _validateBlocExpect(expected, states, shallowEquality); + } - fakeAsync.flushMicrotasks(); - }, - (Object error, _) { - try { - _validateBlocErrors( - errors, - error, - unhandledErrors, - shallowEquality, - ); - } catch (_) { - errorThrown = true; - rethrow; - } - }, - )); + 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())); } @@ -495,7 +497,8 @@ void _validateBlocErrors( if (shallowEquality && error is test.TestFailure) { // ignore: only_throw_errors throw test.TestFailure( - '''${error.message} + ''' +${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''', );