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 : /// [build] should be used for all `bloc` initialization and preparation
14 : /// and must return the `bloc` under test.
15 : ///
16 : /// [seed] is an optional `Function` that returns a state
17 : /// which will be used to seed the `bloc` before [act] is called.
18 : ///
19 : /// [act] is an optional callback which will be invoked with the `bloc` under
20 : /// test and should be used to interact with the `bloc`.
21 : ///
22 : /// [skip] is an optional `int` which can be used to skip any number of states.
23 : /// [skip] defaults to 0.
24 : ///
25 : /// [wait] is an optional `Duration` which can be used to wait for
26 : /// async operations within the `bloc` under test such as `debounceTime`.
27 : ///
28 : /// [expect] is an optional `Function` that returns a `Matcher` which the `bloc`
29 : /// under test is expected to emit after [act] is executed.
30 : ///
31 : /// [verify] is an optional callback which is invoked after [expect]
32 : /// and can be used for additional verification/assertions.
33 : /// [verify] is called with the `bloc` returned by [build].
34 : ///
35 : /// [errors] is an optional `Function` that returns a `Matcher` which the `bloc`
36 : /// under test is expected to throw after [act] is executed.
37 : ///
38 : /// ```dart
39 : /// blocTest(
40 : /// 'CounterBloc emits [1] when increment is added',
41 : /// build: () => CounterBloc(),
42 : /// act: (bloc) => bloc.add(CounterEvent.increment),
43 : /// expect: () => [1],
44 : /// );
45 : /// ```
46 : ///
47 : /// [blocTest] can optionally be used with a seeded state.
48 : ///
49 : /// ```dart
50 : /// blocTest(
51 : /// 'CounterBloc emits [10] when seeded with 9',
52 : /// build: () => CounterBloc(),
53 : /// seed: () => 9,
54 : /// act: (bloc) => bloc.add(CounterEvent.increment),
55 : /// expect: () => [10],
56 : /// );
57 : /// ```
58 : ///
59 : /// [blocTest] can also be used to [skip] any number of emitted states
60 : /// before asserting against the expected states.
61 : /// [skip] defaults to 0.
62 : ///
63 : /// ```dart
64 : /// blocTest(
65 : /// 'CounterBloc emits [2] when increment is added twice',
66 : /// build: () => CounterBloc(),
67 : /// act: (bloc) {
68 : /// bloc
69 : /// ..add(CounterEvent.increment)
70 : /// ..add(CounterEvent.increment);
71 : /// },
72 : /// skip: 1,
73 : /// expect: () => [2],
74 : /// );
75 : /// ```
76 : ///
77 : /// [blocTest] can also be used to wait for async operations
78 : /// by optionally providing a `Duration` to [wait].
79 : ///
80 : /// ```dart
81 : /// blocTest(
82 : /// 'CounterBloc emits [1] when increment is added',
83 : /// build: () => CounterBloc(),
84 : /// act: (bloc) => bloc.add(CounterEvent.increment),
85 : /// wait: const Duration(milliseconds: 300),
86 : /// expect: () => [1],
87 : /// );
88 : /// ```
89 : ///
90 : /// [blocTest] can also be used to [verify] internal bloc functionality.
91 : ///
92 : /// ```dart
93 : /// blocTest(
94 : /// 'CounterBloc emits [1] when increment is added',
95 : /// build: () => CounterBloc(),
96 : /// act: (bloc) => bloc.add(CounterEvent.increment),
97 : /// expect: () => [1],
98 : /// verify: (_) {
99 : /// verify(() => repository.someMethod(any())).called(1);
100 : /// }
101 : /// );
102 : /// ```
103 : ///
104 : /// **Note:** when using [blocTest] with state classes which don't override
105 : /// `==` and `hashCode` you can provide an `Iterable` of matchers instead of
106 : /// explicit state instances.
107 : ///
108 : /// ```dart
109 : /// blocTest(
110 : /// 'emits [StateB] when EventB is added',
111 : /// build: () => MyBloc(),
112 : /// act: (bloc) => bloc.add(EventB()),
113 : /// expect: () => [isA<StateB>()],
114 : /// );
115 : /// ```
116 1 : @isTest
117 : void blocTest<B extends BlocBase<State>, State>(
118 : String description, {
119 : required B Function() build,
120 : State Function()? seed,
121 : Function(B bloc)? act,
122 : Duration? wait,
123 : int skip = 0,
124 : dynamic Function()? expect,
125 : Function(B bloc)? verify,
126 : dynamic Function()? errors,
127 : }) {
128 2 : test.test(description, () async {
129 2 : await testBloc<B, State>(
130 : build: build,
131 : seed: seed,
132 : act: act,
133 : wait: wait,
134 : skip: skip,
135 : expect: expect,
136 : verify: verify,
137 : errors: errors,
138 : );
139 : });
140 : }
141 :
142 : /// Internal [blocTest] runner which is only visible for testing.
143 : /// This should never be used directly -- please use [blocTest] instead.
144 : @visibleForTesting
145 1 : Future<void> testBloc<B extends BlocBase<State>, State>({
146 : required B Function() build,
147 : State Function()? seed,
148 : Function(B bloc)? act,
149 : Duration? wait,
150 : int skip = 0,
151 : dynamic Function()? expect,
152 : Function(B bloc)? verify,
153 : dynamic Function()? errors,
154 : }) async {
155 1 : final unhandledErrors = <Object>[];
156 : var shallowEquality = false;
157 2 : await runZonedGuarded(
158 1 : () async {
159 1 : final states = <State>[];
160 1 : final bloc = build();
161 : // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
162 2 : if (seed != null) bloc.emit(seed());
163 4 : final subscription = bloc.stream.skip(skip).listen(states.add);
164 : try {
165 2 : await act?.call(bloc);
166 1 : } on Exception catch (error) {
167 1 : unhandledErrors.add(
168 2 : error is BlocUnhandledErrorException ? error.error : error,
169 : );
170 : }
171 2 : if (wait != null) await Future<void>.delayed(wait);
172 2 : await Future<void>.delayed(Duration.zero);
173 2 : await bloc.close();
174 : if (expect != null) {
175 1 : final dynamic expected = expect();
176 2 : shallowEquality = '$states' == '$expected';
177 2 : test.expect(states, test.wrapMatcher(expected));
178 : }
179 2 : await subscription.cancel();
180 2 : await verify?.call(bloc);
181 : },
182 1 : (Object error, _) {
183 1 : if (error is BlocUnhandledErrorException) {
184 2 : unhandledErrors.add(error.error);
185 1 : } else if (shallowEquality && error is test.TestFailure) {
186 : // ignore: only_throw_errors
187 1 : throw test.TestFailure(
188 1 : '''${error.message}
189 : WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable.
190 1 : Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n''',
191 : );
192 : } else {
193 : // ignore: only_throw_errors
194 : throw error;
195 : }
196 : },
197 : );
198 3 : if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors()));
199 : }
|