OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 // TODO(nweiz): Keep track of and display multiple errors so there's more | |
6 // visibility into cascading errors. | |
7 // TODO(nweiz): Add timeouts to scheduled tests. | |
8 // TODO(nweiz): Add support for calling [schedule] while the schedule is already | |
9 // running. | |
10 // TODO(nweiz): Port the non-Pub-specific scheduled test libraries from Pub. | |
11 /// A package for writing readable tests of asynchronous behavior. | |
12 /// | |
13 /// This package works by building up a queue of asynchronous tasks called a | |
14 /// "schedule", then executing those tasks in order. This allows the tests to | |
15 /// read like synchronous, linear code, despite executing asynchronously. | |
16 /// | |
17 /// The `scheduled_test` package is built on top of `unittest`, and should be | |
18 /// imported instead of `unittest`. It provides its own version of [group], | |
19 /// [test], and [setUp], and re-exports most other APIs from unittest. | |
20 /// | |
21 /// To schedule a task, call the [schedule] function. For example: | |
22 /// | |
23 /// import 'package:scheduled_test/scheduled_test.dart'; | |
24 /// | |
25 /// void main() { | |
26 /// test('writing to a file and reading it back should work', () { | |
27 /// schedule(() { | |
28 /// // The schedule won't proceed until the returned Future has | |
29 /// // completed. | |
30 /// return new File("output.txt").writeAsString("contents"); | |
31 /// }); | |
32 /// | |
33 /// schedule(() { | |
34 /// return new File("output.txt").readAsString().then((contents) { | |
35 /// // The normal unittest matchers can still be used. | |
36 /// expect(contents, equals("contents")); | |
37 /// }); | |
38 /// }); | |
39 /// }); | |
40 /// } | |
41 /// | |
42 /// ## Setting Up and Tearing Down | |
43 /// | |
44 /// The `scheduled_test` package defines its own [setUp] method that works just | |
45 /// like the one in `unittest`. Tasks can be scheduled in [setUp]; they'll be | |
46 /// run before the tasks scheduled by tests in that group. [currentSchedule] is | |
47 /// also set in the [setUp] callback. | |
48 /// | |
49 /// This package doesn't have an explicit `tearDown` method. Instead, the | |
50 /// [currentSchedule.onComplete] and [currentSchedule.onException] task queues | |
51 /// can have tasks scheduled during [setUp]. For example: | |
52 /// | |
53 /// import 'package:scheduled_test/scheduled_test.dart'; | |
54 /// | |
55 /// void main() { | |
56 /// var tempDir; | |
57 /// setUp(() { | |
58 /// schedule(() { | |
59 /// return createTempDir().then((dir) { | |
60 /// tempDir = dir; | |
61 /// }); | |
62 /// }); | |
63 /// | |
64 /// currentSchedule.onComplete.schedule(() => deleteDir(tempDir)); | |
65 /// }); | |
66 /// | |
67 /// // ... | |
68 /// } | |
69 /// | |
70 /// ## Passing Values Between Tasks | |
71 /// | |
72 /// It's often useful to use values computed in one task in other tasks that are | |
73 /// scheduled afterwards. There are two ways to do this. The most | |
74 /// straightforward is just to define a local variable and assign to it. For | |
75 /// example: | |
76 /// | |
77 /// import 'package:scheduled_test/scheduled_test.dart'; | |
78 /// | |
79 /// void main() { | |
80 /// test('computeValue returns 12', () { | |
81 /// var value; | |
82 /// | |
83 /// schedule(() { | |
84 /// return computeValue().then((computedValue) { | |
85 /// value = computedValue; | |
86 /// }); | |
87 /// }); | |
88 /// | |
89 /// schedule(() => expect(value, equals(12))); | |
90 /// }); | |
91 /// } | |
92 /// | |
93 /// However, this doesn't scale well, especially when you start factoring out | |
94 /// calls to [schedule] into library methods. For that reason, [schedule] | |
95 /// returns a [Future] that will complete to the same value as the return | |
96 /// value of the task. For example: | |
97 /// | |
98 /// import 'package:scheduled_test/scheduled_test.dart'; | |
99 /// | |
100 /// void main() { | |
101 /// test('computeValue returns 12', () { | |
102 /// var valueFuture = schedule(() => computeValue()); | |
103 /// schedule(() { | |
104 /// valueFuture.then((value) => expect(value, equals(12))); | |
105 /// }); | |
106 /// }); | |
107 /// } | |
108 /// | |
109 /// ## Out-of-Band Callbacks | |
110 /// | |
111 /// Sometimes your tests will have callbacks that don't fit into the schedule. | |
112 /// It's important that errors in these callbacks are still registered, though, | |
113 /// and that [Schedule.onException] and [Schedule.onComplete] still run after | |
114 /// they finish. When using `unittest`, you wrap these callbacks with | |
Bob Nystrom
2013/02/08 16:15:37
<3 <3 <3
| |
115 /// `expectAsyncN`; when using `scheduled_test`, you use [wrapAsync]. | |
116 /// | |
117 /// [wrapAsync] has two important functions. First, any errors that occur in it | |
118 /// will be passed into the [Schedule] instead of causing the whole test to | |
119 /// crash. They can then be handled by [Schedule.onException] and | |
120 /// [Schedule.onComplete]. Second, a task queue isn't considered finished until | |
121 /// all of its [wrapAsync]-wrapped functions have been called. This ensures that | |
Bob Nystrom
2013/02/08 16:15:37
We'll probably eventually want the ability to wrap
nweiz
2013/02/08 22:14:38
Once that comes up, we can port guardAsync as well
| |
122 /// [Schedule.onException] and [Schedule.onComplete] will always run after all | |
123 /// the test code in the main queue. | |
124 /// | |
125 /// Note that the [completes], [completion], and [throws] matchers use | |
126 /// [wrapAsync] internally, so they're safe to use in conjunction with scheduled | |
127 /// tests. | |
128 /// | |
129 /// Here's an example of a test using [wrapAsync] to catch errors thrown in the | |
130 /// callback of a fictional `startServer` function: | |
131 /// | |
132 /// import 'package:scheduled_test/scheduled_test.dart'; | |
133 /// | |
134 /// void main() { | |
135 /// test('sendRequest sends a request', () { | |
136 /// startServer(wrapAsync((request) { | |
137 /// expect(request.body, equals('payload')); | |
138 /// request.response.close(); | |
139 /// })); | |
140 /// | |
141 /// schedule(() => sendRequest('payload')); | |
142 /// }); | |
143 /// } | |
144 library scheduled_test; | |
145 | |
146 import 'dart:async'; | |
147 | |
148 import 'package:unittest/unittest.dart' as unittest; | |
149 | |
150 import 'src/schedule.dart'; | |
151 import 'src/schedule_error.dart'; | |
152 import 'src/utils.dart'; | |
153 | |
154 export 'package:unittest/matcher.dart'; | |
155 export 'package:unittest/unittest.dart' show | |
156 config, configure, Configuration, logMessage, expectThrow, fail; | |
157 | |
158 export 'src/schedule.dart'; | |
159 export 'src/schedule_error.dart'; | |
160 export 'src/task.dart'; | |
161 | |
162 /// The [Schedule] for the current test. This is used to add new tasks and | |
163 /// inspect the state of the schedule. | |
164 /// | |
165 /// This is `null` when there's no test currently running. | |
Bob Nystrom
2013/02/08 16:15:37
Maybe throw if the user tries to access this outsi
nweiz
2013/02/08 22:14:38
I think it's useful to allow users to check if a s
| |
166 Schedule get currentSchedule => _currentSchedule; | |
167 Schedule _currentSchedule; | |
168 | |
169 /// Creates a new test case with the given description and body. This has the | |
170 /// same semantics as [unittest.test]. | |
171 void test(String description, void body()) => | |
172 _test(description, body, unittest.test); | |
173 | |
174 /// Creates a new test case with the given description and body that will be the | |
175 /// only test run in this file. This has the same semantics as | |
176 /// [unittest.solo_test]. | |
177 void solo_test(String description, void body()) => | |
178 _test(description, body, unittest.solo_test); | |
179 | |
180 void _test(String description, void body(), [Function testFn]) { | |
Bob Nystrom
2013/02/08 16:15:37
Why is testFn optional?
nweiz
2013/02/08 22:14:38
Good question. Fixed.
| |
181 _ensureInitialized(); | |
182 _ensureSetUpForTopLevel(); | |
183 testFn(description, () { | |
184 var asyncDone = unittest.expectAsync0(() {}); | |
185 return currentSchedule.run(body).then((_) { | |
186 // If we got here, the test completed successfully so tell unittest so. | |
187 asyncDone(); | |
188 }).catchError((e) { | |
189 if (e is ScheduleError) { | |
190 unittest.registerException(new ExpectException(e.toString())); | |
191 } else if (e is AsyncError) { | |
192 unittest.registerException(e.error, e.stackTrace); | |
193 } else { | |
194 unittest.registerException(e); | |
195 } | |
196 }); | |
197 }); | |
198 } | |
199 | |
200 /// Whether or not the tests currently being defined are in a group. This is | |
201 /// only true when defining tests, not when executing them. | |
202 bool _inGroup = false; | |
203 | |
204 /// Creates a new named group of tests. This has the same semantics as | |
205 /// [unittest.group]. | |
206 void group(String description, void body()) { | |
207 unittest.group(description, () { | |
208 var wasInGroup = _inGroup; | |
209 _inGroup = true; | |
210 _setUpScheduledTest(); | |
211 body(); | |
212 _inGroup = wasInGroup; | |
213 }); | |
214 } | |
215 | |
216 /// Schedules a task, [fn], to run asynchronously as part of the main task queue | |
217 /// of [currentSchedule]. Tasks will be run in the order they're scheduled. In | |
Bob Nystrom
2013/02/08 16:15:37
In -> If
nweiz
2013/02/08 22:14:38
Done.
| |
218 /// [fn] returns a [Future], tasks after it won't be run until that [Future] | |
219 /// completes. | |
220 /// | |
221 /// The return value will be completed once the scheduled task has finished | |
222 /// running. Its return value is the same as the return value of [fn], or the | |
223 /// value it completes to if it's a [Future]. | |
224 /// | |
225 /// If [description] is passed, it's used to describe the task for debugging | |
226 /// purposes when an error occurs. | |
227 /// | |
228 /// This function is identical to [currentSchedule.tasks.schedule]. | |
229 Future schedule(fn(), [String description]) => | |
230 currentSchedule.tasks.schedule(fn, description); | |
231 | |
232 /// Register a [setUp] function for a test [group]. This has the same semantics | |
233 /// as [unittest.setUp]. Tasks may be scheduled using [schedule] within | |
234 /// [setUpFn], and [currentSchedule] may be accessed as well. | |
235 /// | |
236 /// Note that there is no associated [tearDown] function. Instead, tasks should | |
237 /// be scheduled for [currentSchedule.onComplete] or | |
238 /// [currentSchedule.onException]. These tasks will be run after each test's | |
239 /// schedule is completed. | |
240 void setUp(void setUpFn()) { | |
241 _setUpScheduledTest(setUpFn); | |
242 } | |
243 | |
244 /// Whether [unittest.setUp] has been called in the top level scope. | |
245 bool _setUpForTopLevel = false; | |
246 | |
247 /// If we're in the top-level scope (that is, not in any [group]s) and | |
248 /// [unittest.setUp] hasn't been called yet, call it. | |
249 void _ensureSetUpForTopLevel() { | |
250 if (_inGroup || _setUpForTopLevel) return; | |
251 _setUpScheduledTest(); | |
252 } | |
253 | |
254 /// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up | |
255 /// and tear down the scheduled test infrastructure. | |
256 void _setUpScheduledTest([void setUpFn()]) { | |
257 if (!_inGroup) _setUpForTopLevel = true; | |
258 | |
259 unittest.setUp(() { | |
260 if (currentSchedule != null) { | |
261 throw 'There seems to be another scheduled test "$description" still ' | |
Bob Nystrom
2013/02/08 16:15:37
Throw a StateError.
nweiz
2013/02/08 22:14:38
Done.
| |
262 'running.'; | |
263 } | |
264 _currentSchedule = new Schedule(); | |
265 if (setUpFn != null) setUpFn(); | |
266 }); | |
267 | |
268 unittest.tearDown(() { | |
269 _currentSchedule = null; | |
270 }); | |
271 } | |
272 | |
273 /// Ensures that the global configuration for `scheduled_test` has been | |
274 /// initialized. | |
275 void _ensureInitialized() { | |
276 unittest.ensureInitialized(); | |
277 unittest.wrapAsync = (f) { | |
278 if (currentSchedule == null) { | |
279 throw "Unexpected call to wrapAsync with no current schedule."; | |
Bob Nystrom
2013/02/08 16:15:37
Throw a StateError.
nweiz
2013/02/08 22:14:38
Done.
| |
280 } | |
281 | |
282 return currentSchedule.wrapAsync(f); | |
283 }; | |
284 } | |
OLD | NEW |