Chromium Code Reviews| 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 |