Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:bloc/bloc.dart';
4 : import 'package:meta/meta.dart';
5 : import 'package:test/test.dart' as test;
6 :
7 : /// Creates a new `bloc`-specific test case with the given [description].
8 : /// [blocTest] will handle asserting that the `bloc` emits the [expect]ed
9 : /// states (in order) after [act] is executed.
10 : /// [blocTest] also handles ensuring that no additional states are emitted
11 : /// by closing the `bloc` stream before evaluating the [expect]ation.
12 : ///
13 : /// [setUp] is optional and should be used to set up
14 : /// any dependencies prior to initializing the `bloc` under test.
15 : /// [setUp] should be used to set up state necessary for a particular test case.
16 : /// For common set up code, prefer to use `setUp` from `package:test/test.dart`.
17 : ///
18 : /// [build] should construct and return the `bloc` under test.
19 : ///
20 : /// [seed] is an optional `Function` that returns a state
21 : /// which will be used to seed the `bloc` before [act] is called.
22 : ///
23 : /// [act] is an optional callback which will be invoked with the `bloc` under
24 : /// test and should be used to interact with the `bloc`.
25 : ///
26 : /// [skip] is an optional `int` which can be used to skip any number of states.
27 : /// [skip] defaults to 0.
28 : ///
29 : /// [wait] is an optional `Duration` which can be used to wait for
30 : /// async operations within the `bloc` under test such as `debounceTime`.
31 : ///
32 : /// [expect] is an optional `Function` that returns a `Matcher` which the `bloc`
33 : /// under test is expected to emit after [act] is executed.
34 : ///
35 : /// [verify] is an optional callback which is invoked after [expect]
36 : /// and can be used for additional verification/assertions.
37 : /// [verify] is called with the `bloc` returned by [build].
38 : ///
39 : /// [errors] is an optional `Function` that returns a `Matcher` which the `bloc`
40 : /// under test is expected to throw after [act] is executed.
41 : ///
42 : /// [tearDown] is optional and can be used to
43 : /// execute any code after the test has run.
44 : /// [tearDown] should be used to clean up after a particular test case.
45 : /// For common tear down code, prefer to use `tearDown` from `package:test/test.dart`.
46 : ///
47 : /// [tags] is optional and if it is passed, it declares user-defined tags
48 : /// that are applied to the test. These tags can be used to select or
49 : /// skip the test on the command line, or to do bulk test configuration.
50 : ///
51 : /// ```dart
52 : /// blocTest(
53 : /// 'CounterBloc emits [1] when increment is added',
54 : /// build: () => CounterBloc(),
55 : /// act: (bloc) => bloc.add(CounterEvent.increment),
56 : /// expect: () => [1],
57 : /// );
58 : /// ```
59 : ///
60 : /// [blocTest] can optionally be used with a seeded state.
61 : ///
62 : /// ```dart
63 : /// blocTest(
64 : /// 'CounterBloc emits [10] when seeded with 9',
65 : /// build: () => CounterBloc(),
66 : /// seed: () => 9,
67 : /// act: (bloc) => bloc.add(CounterEvent.increment),
68 : /// expect: () => [10],
69 : /// );
70 : /// ```
71 : ///
72 : /// [blocTest] can also be used to [skip] any number of emitted states
73 : /// before asserting against the expected states.
74 : /// [skip] defaults to 0.
75 : ///
76 : /// ```dart
77 : /// blocTest(
78 : /// 'CounterBloc emits [2] when increment is added twice',
79 : /// build: () => CounterBloc(),
80 : /// act: (bloc) {
81 : /// bloc
82 : /// ..add(CounterEvent.increment)
83 : /// ..add(CounterEvent.increment);
84 : /// },
85 : /// skip: 1,
86 : /// expect: () => [2],
87 : /// );
88 : /// ```
89 : ///
90 : /// [blocTest] can also be used to wait for async operations
91 : /// by optionally providing a `Duration` to [wait].
92 : ///
93 : /// ```dart
94 : /// blocTest(
95 : /// 'CounterBloc emits [1] when increment is added',
96 : /// build: () => CounterBloc(),
97 : /// act: (bloc) => bloc.add(CounterEvent.increment),
98 : /// wait: const Duration(milliseconds: 300),
99 : /// expect: () => [1],
100 : /// );
101 : /// ```
102 : ///
103 : /// [blocTest] can also be used to [verify] internal bloc functionality.
104 : ///
105 : /// ```dart
106 : /// blocTest(
107 : /// 'CounterBloc emits [1] when increment is added',
108 : /// build: () => CounterBloc(),
109 : /// act: (bloc) => bloc.add(CounterEvent.increment),
110 : /// expect: () => [1],
111 : /// verify: (_) {
112 : /// verify(() => repository.someMethod(any())).called(1);
113 : /// }
114 : /// );
115 : /// ```
116 : ///
117 : /// **Note:** when using [blocTest] with state classes which don't override
118 : /// `==` and `hashCode` you can provide an `Iterable` of matchers instead of
119 : /// explicit state instances.
120 : ///
121 : /// ```dart
122 : /// blocTest(
123 : /// 'emits [StateB] when EventB is added',
124 : /// build: () => MyBloc(),
125 : /// act: (bloc) => bloc.add(EventB()),
126 : /// expect: () => [isA<StateB>()],
127 : /// );
128 : /// ```
129 : ///
130 : /// If [tags] is passed, it declares user-defined tags that are applied to the
131 : /// test. These tags can be used to select or skip the test on the command line,
132 : /// or to do bulk test configuration. All tags should be declared in the
133 : /// [package configuration file][configuring tags]. The parameter can be an
134 : /// [Iterable] of tag names, or a [String] representing a single tag.
135 : ///
136 : /// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
137 1 : @isTest
138 : void blocTest<B extends BlocBase<State>, State>(
139 : String description, {
140 : FutureOr<void> Function()? setUp,
141 : required B Function() build,
142 : State Function()? seed,
143 : Function(B bloc)? act,
144 : Duration? wait,
145 : int skip = 0,
146 : dynamic Function()? expect,
147 : Function(B bloc)? verify,
148 : dynamic Function()? errors,
149 : FutureOr<void> Function()? tearDown,
150 : dynamic tags,
151 : }) {
152 1 : test.test(
153 : description,
154 1 : () async {
155 2 : await testBloc<B, State>(
156 : setUp: setUp,
157 : build: build,
158 : seed: seed,
159 : act: act,
160 : wait: wait,
161 : skip: skip,
162 : expect: expect,
163 : verify: verify,
164 : errors: errors,
165 : tearDown: tearDown,
166 : );
167 : },
168 : tags: tags,
169 : );
170 : }
171 :
172 : /// Internal [blocTest] runner which is only visible for testing.
173 : /// This should never be used directly -- please use [blocTest] instead.
174 : @visibleForTesting
175 1 : Future<void> testBloc<B extends BlocBase<State>, State>({
176 : FutureOr<void> Function()? setUp,
177 : required B Function() build,
178 : State Function()? seed,
179 : Function(B bloc)? act,
180 : Duration? wait,
181 : int skip = 0,
182 : dynamic Function()? expect,
183 : Function(B bloc)? verify,
184 : dynamic Function()? errors,
185 : FutureOr<void> Function()? tearDown,
186 : }) async {
187 1 : final unhandledErrors = <Object>[];
188 : var shallowEquality = false;
189 1 : final localObserver = Bloc.observer;
190 2 : final testObserver = _TestBlocObserver(localObserver, unhandledErrors.add);
191 : Bloc.observer = testObserver;
192 2 : await runZonedGuarded(
193 1 : () async {
194 1 : await setUp?.call();
195 1 : final states = <State>[];
196 : final bloc = build();
197 : // ignore: invalid_use_of_protected_member
198 1 : if (seed != null) bloc.emit(seed());
199 4 : final subscription = bloc.stream.skip(skip).listen(states.add);
200 : try {
201 1 : await act?.call(bloc);
202 : } catch (error) {
203 1 : unhandledErrors.add(error);
204 : }
205 2 : if (wait != null) await Future<void>.delayed(wait);
206 2 : await Future<void>.delayed(Duration.zero);
207 2 : await bloc.close();
208 : if (expect != null) {
209 : final dynamic expected = expect();
210 2 : shallowEquality = '$states' == '$expected';
211 2 : test.expect(states, test.wrapMatcher(expected));
212 : }
213 2 : await subscription.cancel();
214 1 : await verify?.call(bloc);
215 1 : await tearDown?.call();
216 : },
217 1 : (Object error, _) {
218 1 : if (shallowEquality && error is test.TestFailure) {
219 : // ignore: only_throw_errors
220 1 : throw test.TestFailure(
221 1 : '''${error.message}
222 : WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable.
223 1 : Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n''',
224 : );
225 : }
226 : // ignore: only_throw_errors
227 : if (errors == null) throw error;
228 0 : unhandledErrors.add(error);
229 : },
230 : );
231 2 : if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors()));
232 : Bloc.observer = localObserver;
233 : }
234 :
235 : class _TestBlocObserver extends BlocObserver {
236 1 : _TestBlocObserver(this.localObserver, this._onError);
237 :
238 : final BlocObserver localObserver;
239 : final void Function(Object error) _onError;
240 :
241 1 : @override
242 : void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
243 2 : localObserver.onError(bloc, error, stackTrace);
244 1 : _onError(error);
245 1 : super.onError(bloc, error, stackTrace);
246 : }
247 : }
|