-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(bloc_test): testBlocFakeAsync - fire all asynchronous events without actually needing to wait for real time to elapse #3796
base: master
Are you sure you want to change the base?
Changes from all commits
a90d51d
d0fc9a6
f5af29f
e71865a
8b7fbc9
5a4a9f6
4ee0f86
f19316f
c73593f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
|
||
|
@@ -216,39 +217,314 @@ Future<void> testBloc<B extends BlocBase<State>, 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<State>) rethrow; | ||
final diff = _diff(expected: expected, actual: states); | ||
final message = '${e.message}\n$diff'; | ||
// ignore: only_throw_errors | ||
throw test.TestFailure(message); | ||
} | ||
_validateBlocExpect<State>(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<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, { | ||
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<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>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method duplicates a lot of code that can be found on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tenhobi @mugbug there's a few reasons to not unify the code:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
||
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 = <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'; | ||
_validateBlocExpect<State>(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<Object> 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<State>( | ||
dynamic expected, | ||
List<State> states, | ||
bool shallowEquality, | ||
) { | ||
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); | ||
} | ||
} | ||
|
||
class _TestBlocObserver extends BlocObserver { | ||
const _TestBlocObserver(this._localObserver, this._onError); | ||
|
||
|
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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