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 |
| 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 |
| 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. |
| 166 Schedule get currentSchedule => _currentSchedule; |
| 167 Schedule _currentSchedule; |
| 168 |
| 169 /// The user-provided setUp function. This is set for each test during |
| 170 /// `unittest.setUp`. |
| 171 Function _setUpFn; |
| 172 |
| 173 /// Creates a new test case with the given description and body. This has the |
| 174 /// same semantics as [unittest.test]. |
| 175 void test(String description, void body()) => |
| 176 _test(description, body, unittest.test); |
| 177 |
| 178 /// Creates a new test case with the given description and body that will be the |
| 179 /// only test run in this file. This has the same semantics as |
| 180 /// [unittest.solo_test]. |
| 181 void solo_test(String description, void body()) => |
| 182 _test(description, body, unittest.solo_test); |
| 183 |
| 184 void _test(String description, void body(), Function testFn) { |
| 185 _ensureInitialized(); |
| 186 _ensureSetUpForTopLevel(); |
| 187 testFn(description, () { |
| 188 var asyncDone = unittest.expectAsync0(() {}); |
| 189 return currentSchedule.run(() { |
| 190 if (_setUpFn != null) _setUpFn(); |
| 191 body(); |
| 192 }).then((_) { |
| 193 // If we got here, the test completed successfully so tell unittest so. |
| 194 asyncDone(); |
| 195 }).catchError((e) { |
| 196 if (e is ScheduleError) { |
| 197 unittest.registerException(new ExpectException(e.toString())); |
| 198 } else if (e is AsyncError) { |
| 199 unittest.registerException(e.error, e.stackTrace); |
| 200 } else { |
| 201 unittest.registerException(e); |
| 202 } |
| 203 }); |
| 204 }); |
| 205 } |
| 206 |
| 207 /// Whether or not the tests currently being defined are in a group. This is |
| 208 /// only true when defining tests, not when executing them. |
| 209 bool _inGroup = false; |
| 210 |
| 211 /// Creates a new named group of tests. This has the same semantics as |
| 212 /// [unittest.group]. |
| 213 void group(String description, void body()) { |
| 214 unittest.group(description, () { |
| 215 var wasInGroup = _inGroup; |
| 216 _inGroup = true; |
| 217 _setUpScheduledTest(); |
| 218 body(); |
| 219 _inGroup = wasInGroup; |
| 220 }); |
| 221 } |
| 222 |
| 223 /// Schedules a task, [fn], to run asynchronously as part of the main task queue |
| 224 /// of [currentSchedule]. Tasks will be run in the order they're scheduled. If |
| 225 /// [fn] returns a [Future], tasks after it won't be run until that [Future] |
| 226 /// completes. |
| 227 /// |
| 228 /// The return value will be completed once the scheduled task has finished |
| 229 /// running. Its return value is the same as the return value of [fn], or the |
| 230 /// value it completes to if it's a [Future]. |
| 231 /// |
| 232 /// If [description] is passed, it's used to describe the task for debugging |
| 233 /// purposes when an error occurs. |
| 234 /// |
| 235 /// This function is identical to [currentSchedule.tasks.schedule]. |
| 236 Future schedule(fn(), [String description]) => |
| 237 currentSchedule.tasks.schedule(fn, description); |
| 238 |
| 239 /// Register a [setUp] function for a test [group]. This has the same semantics |
| 240 /// as [unittest.setUp]. Tasks may be scheduled using [schedule] within |
| 241 /// [setUpFn], and [currentSchedule] may be accessed as well. |
| 242 /// |
| 243 /// Note that there is no associated [tearDown] function. Instead, tasks should |
| 244 /// be scheduled for [currentSchedule.onComplete] or |
| 245 /// [currentSchedule.onException]. These tasks will be run after each test's |
| 246 /// schedule is completed. |
| 247 void setUp(void setUpFn()) { |
| 248 _setUpScheduledTest(setUpFn); |
| 249 } |
| 250 |
| 251 /// Whether [unittest.setUp] has been called in the top level scope. |
| 252 bool _setUpForTopLevel = false; |
| 253 |
| 254 /// If we're in the top-level scope (that is, not in any [group]s) and |
| 255 /// [unittest.setUp] hasn't been called yet, call it. |
| 256 void _ensureSetUpForTopLevel() { |
| 257 if (_inGroup || _setUpForTopLevel) return; |
| 258 _setUpScheduledTest(); |
| 259 } |
| 260 |
| 261 /// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up |
| 262 /// and tear down the scheduled test infrastructure. |
| 263 void _setUpScheduledTest([void setUpFn()]) { |
| 264 if (!_inGroup) _setUpForTopLevel = true; |
| 265 |
| 266 unittest.setUp(() { |
| 267 if (currentSchedule != null) { |
| 268 throw new StateError('There seems to be another scheduled test ' |
| 269 'still running.'); |
| 270 } |
| 271 _currentSchedule = new Schedule(); |
| 272 _setUpFn = setUpFn; |
| 273 }); |
| 274 |
| 275 unittest.tearDown(() { |
| 276 _currentSchedule = null; |
| 277 }); |
| 278 } |
| 279 |
| 280 /// Ensures that the global configuration for `scheduled_test` has been |
| 281 /// initialized. |
| 282 void _ensureInitialized() { |
| 283 unittest.ensureInitialized(); |
| 284 unittest.wrapAsync = (f) { |
| 285 if (currentSchedule == null) { |
| 286 throw new StateError("Unexpected call to wrapAsync with no current " |
| 287 "schedule."); |
| 288 } |
| 289 |
| 290 return currentSchedule.wrapAsync(f); |
| 291 }; |
| 292 } |
OLD | NEW |