Skip to content
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 268 additions & 0 deletions packages/bloc_test/lib/src/bloc_test.dart
Original file line number Diff line number Diff line change
@@ -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,273 @@ void blocTest<B extends BlocBase<State>, State>(
);
}

/// Creates a new `bloc`-specific test case with the given [description].
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could also add some sort of example using fakeAsync.elapse which is kind of the main purpose of this new method, wdyt?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might wanna update the README.md docs as well and any other sort of documentation bloc_test might have

/// [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<StateB>()],
/// );
/// ```
///
/// 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<B extends BlocBase<State>, 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<B, State>(
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<B extends BlocBase<State>, State>({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method duplicates a lot of code that can be found on testBloc. What about refactoring both to share as much code as possible so maintenance is simpler?

Copy link
Author

@PankovSerge PankovSerge Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tenhobi @mugbug there's a few reasons to not unify the code:

  1. Breaking change
  2. fakeAsync require sync execution flow, which lead us to critical differences as:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I get it, but my suggestion was to share internal code, not external. So we'd still have the current blocTest and testBlocFakeAsync methods, but the internal code of them will be somehow shared. In this case we won't have breaking changes since the public API for blocTest will remain the same. Not sure about the 2nd point but I suppose we can choose which code to share, so we should be good as well. Does it make sense?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got your point, @mugbug The duplicated code has been extracted to a private functions.

cc: @tenhobi

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Now I suppose we just need to wait for an approval on the code solution and then work on some examples and documentation updates. Good job!

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 = <Object>[];
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 = <State>[];
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<State>) 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
1 change: 1 addition & 0 deletions packages/bloc_test/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
691 changes: 691 additions & 0 deletions packages/bloc_test/test/bloc_fake_async_bloc_test_test.dart
Original file line number Diff line number Diff line change
@@ -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>? _) {}

void main() {
group('blocTest', () {
group('CounterBloc', () {
fakeAsyncBlocTest<CounterBloc, int>(
'supports matchers (contains)',
build: () => CounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => contains(1),
);

fakeAsyncBlocTest<CounterBloc, int>(
'supports matchers (containsAll)',
build: () => CounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
expect: () => containsAll(<int>[2, 1]),
);

fakeAsyncBlocTest<CounterBloc, int>(
'supports matchers (containsAllInOrder)',
build: () => CounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
expect: () => containsAllInOrder(<int>[1, 2]),
);

fakeAsyncBlocTest<CounterBloc, int>(
'emits [] when nothing is added',
build: () => CounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<CounterBloc, int>(
'emits [1] when CounterEvent.increment is added',
build: () => CounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
);

fakeAsyncBlocTest<CounterBloc, int>(
'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 <int>[1],
);

fakeAsyncBlocTest<CounterBloc, int>(
'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 <int>[1, 2],
);

fakeAsyncBlocTest<CounterBloc, int>(
'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 <int>[2],
);

fakeAsyncBlocTest<CounterBloc, int>(
'emits [11] when CounterEvent.increment is added and emitted 10',
build: () => CounterBloc(),
seed: () => 10,
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[11],
);

fakeAsyncBlocTest<CounterBloc, int>(
'emits [11] when CounterEvent.increment is added and seed 10',
build: () => CounterBloc(),
seed: () => 10,
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[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<void>();
await runZonedGuarded(() async {
unawaited(Future.delayed(
Duration.zero,
() => testBlocFakeAsync<CounterBloc, int>(
build: () => CounterBloc(),
act: (bloc, f) => bloc.add(CounterEvent.increment),
expect: () => const <int>[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<void>();
await runZonedGuarded(() async {
unawaited(Future.delayed(
Duration.zero,
() => testBlocFakeAsync<ErrorCounterBloc, int>(
build: () => ErrorCounterBloc(),
act: (bloc, f) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
)).then((_) => completer.complete()));
await completer.future;
}, (Object error, _) {
actualError = error;
});
expect(actualError, isA<ErrorCounterBlocError>());
});

test('fails immediately when exception occurs in act', () async {
final exception = Exception('oops');
Object? actualError;
final completer = Completer<void>();

await runZonedGuarded(() async {
unawaited(Future.delayed(
Duration.zero,
() => testBlocFakeAsync<ErrorCounterBloc, int>(
build: () => ErrorCounterBloc(),
act: (_, f) => throw exception,
expect: () => const <int>[1],
)).then((_) => completer.complete()));
await completer.future;
}, (Object error, _) {
actualError = error;
if (!completer.isCompleted) completer.complete();
});
expect(actualError, exception);
});
});

group('AsyncCounterBloc', () {
fakeAsyncBlocTest<AsyncCounterBloc, int>(
'emits [] when nothing is added',
build: () => AsyncCounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<AsyncCounterBloc, int>(
'emits [1] when CounterEvent.increment is added',
build: () => AsyncCounterBloc(),
act: (bloc, fakeAsync) {
bloc.add(CounterEvent.increment);
fakeAsync.elapse(const Duration(milliseconds: 10));
},
expect: () => const <int>[1],
);

fakeAsyncBlocTest<AsyncCounterBloc, int>(
'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 <int>[1, 2],
);

fakeAsyncBlocTest<AsyncCounterBloc, int>(
'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 <int>[2],
);

fakeAsyncBlocTest<AsyncCounterBloc, int>(
'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 <int>[11],
);
});

group('DebounceCounterBloc', () {
fakeAsyncBlocTest<DebounceCounterBloc, int>(
'emits [] when nothing is added',
build: () => DebounceCounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<DebounceCounterBloc, int>(
'emits [1] when CounterEvent.increment is added',
build: () => DebounceCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
wait: const Duration(milliseconds: 300),
expect: () => const <int>[1],
);

fakeAsyncBlocTest<DebounceCounterBloc, int>(
'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 <int>[2],
);

fakeAsyncBlocTest<DebounceCounterBloc, int>(
'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 <int>[11],
);
});

group('InstantEmitBloc', () {
fakeAsyncBlocTest<InstantEmitBloc, int>(
'emits [1] when nothing is added',
build: () => InstantEmitBloc(),
expect: () => const <int>[1],
);

fakeAsyncBlocTest<InstantEmitBloc, int>(
'emits [1, 2] when CounterEvent.increment is added',
build: () => InstantEmitBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1, 2],
);

fakeAsyncBlocTest<InstantEmitBloc, int>(
'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 <int>[1, 2, 3],
);

fakeAsyncBlocTest<InstantEmitBloc, int>(
'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 <int>[3],
);

fakeAsyncBlocTest<InstantEmitBloc, int>(
'emits [11, 12] when CounterEvent.increment is added and seeded 10',
build: () => InstantEmitBloc(),
seed: () => 10,
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[11, 12],
);
});

group('MultiCounterBloc', () {
fakeAsyncBlocTest<MultiCounterBloc, int>(
'emits [] when nothing is added',
build: () => MultiCounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<MultiCounterBloc, int>(
'emits [1, 2] when CounterEvent.increment is added',
build: () => MultiCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1, 2],
);

fakeAsyncBlocTest<MultiCounterBloc, int>(
'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 <int>[1, 2, 3, 4],
);

fakeAsyncBlocTest<MultiCounterBloc, int>(
'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 <int>[4],
);

fakeAsyncBlocTest<MultiCounterBloc, int>(
'emits [11, 12] when CounterEvent.increment is added and emitted 10',
build: () => MultiCounterBloc(),
seed: () => 10,
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[11, 12],
);
});

group('ComplexBloc', () {
fakeAsyncBlocTest<ComplexBloc, ComplexState>(
'emits [] when nothing is added',
build: () => ComplexBloc(),
expect: () => const <ComplexState>[],
);

fakeAsyncBlocTest<ComplexBloc, ComplexState>(
'emits [ComplexStateB] when ComplexEventB is added',
build: () => ComplexBloc(),
act: (bloc, fakeAsync) => bloc.add(ComplexEventB()),
expect: () => [isA<ComplexStateB>()],
);

fakeAsyncBlocTest<ComplexBloc, ComplexState>(
'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] '
'is added and skip: 1',
build: () => ComplexBloc(),
act: (bloc, fakeAsync) => bloc
..add(ComplexEventB())
..add(ComplexEventA()),
skip: 1,
expect: () => [isA<ComplexStateA>()],
);
});
group('ErrorCounterBloc', () {
fakeAsyncBlocTest<ErrorCounterBloc, int>(
'emits [] when nothing is added',
build: () => ErrorCounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'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 <int>[2],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'emits [1] when increment is added',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'throws ErrorCounterBlocException when increment is added',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
errors: () => [isA<ErrorCounterBlocError>()],
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'emits [1] and throws ErrorCounterBlocError '
'when increment is added',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
errors: () => [isA<ErrorCounterBlocError>()],
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'emits [] and throws ErrorCounterBlocError '
'when ErrorCounterBlocError thrown in act',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => throw ErrorCounterBlocError(),
expect: () => const <int>[],
errors: () => [isA<ErrorCounterBlocError>()],
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'emits [1, 2] when increment is added twice',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
expect: () => const <int>[1, 2],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'throws two ErrorCounterBlocErrors '
'when increment is added twice',
build: () => ErrorCounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
errors: () => [
isA<ErrorCounterBlocError>(),
isA<ErrorCounterBlocError>(),
],
);

fakeAsyncBlocTest<ErrorCounterBloc, int>(
'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 <int>[1, 2],
errors: () => [
isA<ErrorCounterBlocError>(),
isA<ErrorCounterBlocError>(),
],
);
});

group('ExceptionCounterBloc', () {
fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'emits [] when nothing is added',
build: () => ExceptionCounterBloc(),
expect: () => const <int>[],
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'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 <int>[2],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'emits [1] when increment is added',
build: () => ExceptionCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'throws ExceptionCounterBlocException when increment is added',
build: () => ExceptionCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
errors: () => [isA<ExceptionCounterBlocException>()],
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'emits [1] and throws ExceptionCounterBlocException '
'when increment is added',
build: () => ExceptionCounterBloc(),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
errors: () => [isA<ExceptionCounterBlocException>()],
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'emits [1, 2] when increment is added twice',
build: () => ExceptionCounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
expect: () => const <int>[1, 2],
errors: () => isNotEmpty,
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'throws two ExceptionCounterBlocExceptions '
'when increment is added twice',
build: () => ExceptionCounterBloc(),
act: (bloc, fakeAsync) => bloc
..add(CounterEvent.increment)
..add(CounterEvent.increment),
errors: () => [
isA<ExceptionCounterBlocException>(),
isA<ExceptionCounterBlocException>(),
],
);

fakeAsyncBlocTest<ExceptionCounterBloc, int>(
'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 <int>[1, 2],
errors: () => [
isA<ExceptionCounterBlocException>(),
isA<ExceptionCounterBlocException>(),
],
);
});

group('SideEffectCounterBloc', () {
late Repository repository;

setUp(() {
repository = MockRepository();
when(() => repository.sideEffect()).thenReturn(null);
});

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'emits [] when nothing is added',
build: () => SideEffectCounterBloc(repository),
expect: () => const <int>[],
);

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'emits [1] when CounterEvent.increment is added',
build: () => SideEffectCounterBloc(repository),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
expect: () => const <int>[1],
verify: (_) {
verify(() => repository.sideEffect()).called(1);
},
);

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'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 <int>[2],
);

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'does not require an expect',
build: () => SideEffectCounterBloc(repository),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
verify: (_) {
verify(() => repository.sideEffect()).called(1);
},
);

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'async verify',
build: () => SideEffectCounterBloc(repository),
act: (bloc, fakeAsync) => bloc.add(CounterEvent.increment),
verify: (_) async {
await Future<void>.delayed(Duration.zero);
verify(() => repository.sideEffect()).called(1);
},
);

fakeAsyncBlocTest<SideEffectCounterBloc, int>(
'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 <int>[],
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<void>();
await runZonedGuarded(() async {
unawaited(Future.delayed(
Duration.zero,
() => testBlocFakeAsync<SideEffectCounterBloc, int>(
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 <Instance of \'ComplexStateA\'> instead of <Instance of \'ComplexStateA\'>\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<void>();
await runZonedGuarded(() async {
unawaited(Future.delayed(
Duration.zero,
() => testBlocFakeAsync<ComplexBloc, ComplexState>(
build: () => ComplexBloc(),
act: (bloc, fakeAsync) => bloc.add(ComplexEventA()),
expect: () => <ComplexState>[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<CounterBloc, int>(
'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));
},
);
});
}