Chromium Code Reviews| Index: pkg/scheduled_test/lib/scheduled_test.dart |
| diff --git a/pkg/scheduled_test/lib/scheduled_test.dart b/pkg/scheduled_test/lib/scheduled_test.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2f127697398cd3c412b4c62af8af90f591a05496 |
| --- /dev/null |
| +++ b/pkg/scheduled_test/lib/scheduled_test.dart |
| @@ -0,0 +1,284 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +// TODO(nweiz): Keep track of and display multiple errors so there's more |
| +// visibility into cascading errors. |
| +// TODO(nweiz): Add timeouts to scheduled tests. |
| +// TODO(nweiz): Add support for calling [schedule] while the schedule is already |
| +// running. |
| +// TODO(nweiz): Port the non-Pub-specific scheduled test libraries from Pub. |
| +/// A package for writing readable tests of asynchronous behavior. |
| +/// |
| +/// This package works by building up a queue of asynchronous tasks called a |
| +/// "schedule", then executing those tasks in order. This allows the tests to |
| +/// read like synchronous, linear code, despite executing asynchronously. |
| +/// |
| +/// The `scheduled_test` package is built on top of `unittest`, and should be |
| +/// imported instead of `unittest`. It provides its own version of [group], |
| +/// [test], and [setUp], and re-exports most other APIs from unittest. |
| +/// |
| +/// To schedule a task, call the [schedule] function. For example: |
| +/// |
| +/// import 'package:scheduled_test/scheduled_test.dart'; |
| +/// |
| +/// void main() { |
| +/// test('writing to a file and reading it back should work', () { |
| +/// schedule(() { |
| +/// // The schedule won't proceed until the returned Future has |
| +/// // completed. |
| +/// return new File("output.txt").writeAsString("contents"); |
| +/// }); |
| +/// |
| +/// schedule(() { |
| +/// return new File("output.txt").readAsString().then((contents) { |
| +/// // The normal unittest matchers can still be used. |
| +/// expect(contents, equals("contents")); |
| +/// }); |
| +/// }); |
| +/// }); |
| +/// } |
| +/// |
| +/// ## Setting Up and Tearing Down |
| +/// |
| +/// The `scheduled_test` package defines its own [setUp] method that works just |
| +/// like the one in `unittest`. Tasks can be scheduled in [setUp]; they'll be |
| +/// run before the tasks scheduled by tests in that group. [currentSchedule] is |
| +/// also set in the [setUp] callback. |
| +/// |
| +/// This package doesn't have an explicit `tearDown` method. Instead, the |
| +/// [currentSchedule.onComplete] and [currentSchedule.onException] task queues |
| +/// can have tasks scheduled during [setUp]. For example: |
| +/// |
| +/// import 'package:scheduled_test/scheduled_test.dart'; |
| +/// |
| +/// void main() { |
| +/// var tempDir; |
| +/// setUp(() { |
| +/// schedule(() { |
| +/// return createTempDir().then((dir) { |
| +/// tempDir = dir; |
| +/// }); |
| +/// }); |
| +/// |
| +/// currentSchedule.onComplete.schedule(() => deleteDir(tempDir)); |
| +/// }); |
| +/// |
| +/// // ... |
| +/// } |
| +/// |
| +/// ## Passing Values Between Tasks |
| +/// |
| +/// It's often useful to use values computed in one task in other tasks that are |
| +/// scheduled afterwards. There are two ways to do this. The most |
| +/// straightforward is just to define a local variable and assign to it. For |
| +/// example: |
| +/// |
| +/// import 'package:scheduled_test/scheduled_test.dart'; |
| +/// |
| +/// void main() { |
| +/// test('computeValue returns 12', () { |
| +/// var value; |
| +/// |
| +/// schedule(() { |
| +/// return computeValue().then((computedValue) { |
| +/// value = computedValue; |
| +/// }); |
| +/// }); |
| +/// |
| +/// schedule(() => expect(value, equals(12))); |
| +/// }); |
| +/// } |
| +/// |
| +/// However, this doesn't scale well, especially when you start factoring out |
| +/// calls to [schedule] into library methods. For that reason, [schedule] |
| +/// returns a [Future] that will complete to the same value as the return |
| +/// value of the task. For example: |
| +/// |
| +/// import 'package:scheduled_test/scheduled_test.dart'; |
| +/// |
| +/// void main() { |
| +/// test('computeValue returns 12', () { |
| +/// var valueFuture = schedule(() => computeValue()); |
| +/// schedule(() { |
| +/// valueFuture.then((value) => expect(value, equals(12))); |
| +/// }); |
| +/// }); |
| +/// } |
| +/// |
| +/// ## Out-of-Band Callbacks |
| +/// |
| +/// Sometimes your tests will have callbacks that don't fit into the schedule. |
| +/// It's important that errors in these callbacks are still registered, though, |
| +/// and that [Schedule.onException] and [Schedule.onComplete] still run after |
| +/// they finish. When using `unittest`, you wrap these callbacks with |
|
Bob Nystrom
2013/02/08 16:15:37
<3 <3 <3
|
| +/// `expectAsyncN`; when using `scheduled_test`, you use [wrapAsync]. |
| +/// |
| +/// [wrapAsync] has two important functions. First, any errors that occur in it |
| +/// will be passed into the [Schedule] instead of causing the whole test to |
| +/// crash. They can then be handled by [Schedule.onException] and |
| +/// [Schedule.onComplete]. Second, a task queue isn't considered finished until |
| +/// 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
|
| +/// [Schedule.onException] and [Schedule.onComplete] will always run after all |
| +/// the test code in the main queue. |
| +/// |
| +/// Note that the [completes], [completion], and [throws] matchers use |
| +/// [wrapAsync] internally, so they're safe to use in conjunction with scheduled |
| +/// tests. |
| +/// |
| +/// Here's an example of a test using [wrapAsync] to catch errors thrown in the |
| +/// callback of a fictional `startServer` function: |
| +/// |
| +/// import 'package:scheduled_test/scheduled_test.dart'; |
| +/// |
| +/// void main() { |
| +/// test('sendRequest sends a request', () { |
| +/// startServer(wrapAsync((request) { |
| +/// expect(request.body, equals('payload')); |
| +/// request.response.close(); |
| +/// })); |
| +/// |
| +/// schedule(() => sendRequest('payload')); |
| +/// }); |
| +/// } |
| +library scheduled_test; |
| + |
| +import 'dart:async'; |
| + |
| +import 'package:unittest/unittest.dart' as unittest; |
| + |
| +import 'src/schedule.dart'; |
| +import 'src/schedule_error.dart'; |
| +import 'src/utils.dart'; |
| + |
| +export 'package:unittest/matcher.dart'; |
| +export 'package:unittest/unittest.dart' show |
| + config, configure, Configuration, logMessage, expectThrow, fail; |
| + |
| +export 'src/schedule.dart'; |
| +export 'src/schedule_error.dart'; |
| +export 'src/task.dart'; |
| + |
| +/// The [Schedule] for the current test. This is used to add new tasks and |
| +/// inspect the state of the schedule. |
| +/// |
| +/// 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
|
| +Schedule get currentSchedule => _currentSchedule; |
| +Schedule _currentSchedule; |
| + |
| +/// Creates a new test case with the given description and body. This has the |
| +/// same semantics as [unittest.test]. |
| +void test(String description, void body()) => |
| + _test(description, body, unittest.test); |
| + |
| +/// Creates a new test case with the given description and body that will be the |
| +/// only test run in this file. This has the same semantics as |
| +/// [unittest.solo_test]. |
| +void solo_test(String description, void body()) => |
| + _test(description, body, unittest.solo_test); |
| + |
| +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.
|
| + _ensureInitialized(); |
| + _ensureSetUpForTopLevel(); |
| + testFn(description, () { |
| + var asyncDone = unittest.expectAsync0(() {}); |
| + return currentSchedule.run(body).then((_) { |
| + // If we got here, the test completed successfully so tell unittest so. |
| + asyncDone(); |
| + }).catchError((e) { |
| + if (e is ScheduleError) { |
| + unittest.registerException(new ExpectException(e.toString())); |
| + } else if (e is AsyncError) { |
| + unittest.registerException(e.error, e.stackTrace); |
| + } else { |
| + unittest.registerException(e); |
| + } |
| + }); |
| + }); |
| +} |
| + |
| +/// Whether or not the tests currently being defined are in a group. This is |
| +/// only true when defining tests, not when executing them. |
| +bool _inGroup = false; |
| + |
| +/// Creates a new named group of tests. This has the same semantics as |
| +/// [unittest.group]. |
| +void group(String description, void body()) { |
| + unittest.group(description, () { |
| + var wasInGroup = _inGroup; |
| + _inGroup = true; |
| + _setUpScheduledTest(); |
| + body(); |
| + _inGroup = wasInGroup; |
| + }); |
| +} |
| + |
| +/// Schedules a task, [fn], to run asynchronously as part of the main task queue |
| +/// 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.
|
| +/// [fn] returns a [Future], tasks after it won't be run until that [Future] |
| +/// completes. |
| +/// |
| +/// The return value will be completed once the scheduled task has finished |
| +/// running. Its return value is the same as the return value of [fn], or the |
| +/// value it completes to if it's a [Future]. |
| +/// |
| +/// If [description] is passed, it's used to describe the task for debugging |
| +/// purposes when an error occurs. |
| +/// |
| +/// This function is identical to [currentSchedule.tasks.schedule]. |
| +Future schedule(fn(), [String description]) => |
| + currentSchedule.tasks.schedule(fn, description); |
| + |
| +/// Register a [setUp] function for a test [group]. This has the same semantics |
| +/// as [unittest.setUp]. Tasks may be scheduled using [schedule] within |
| +/// [setUpFn], and [currentSchedule] may be accessed as well. |
| +/// |
| +/// Note that there is no associated [tearDown] function. Instead, tasks should |
| +/// be scheduled for [currentSchedule.onComplete] or |
| +/// [currentSchedule.onException]. These tasks will be run after each test's |
| +/// schedule is completed. |
| +void setUp(void setUpFn()) { |
| + _setUpScheduledTest(setUpFn); |
| +} |
| + |
| +/// Whether [unittest.setUp] has been called in the top level scope. |
| +bool _setUpForTopLevel = false; |
| + |
| +/// If we're in the top-level scope (that is, not in any [group]s) and |
| +/// [unittest.setUp] hasn't been called yet, call it. |
| +void _ensureSetUpForTopLevel() { |
| + if (_inGroup || _setUpForTopLevel) return; |
| + _setUpScheduledTest(); |
| +} |
| + |
| +/// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up |
| +/// and tear down the scheduled test infrastructure. |
| +void _setUpScheduledTest([void setUpFn()]) { |
| + if (!_inGroup) _setUpForTopLevel = true; |
| + |
| + unittest.setUp(() { |
| + if (currentSchedule != null) { |
| + 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.
|
| + 'running.'; |
| + } |
| + _currentSchedule = new Schedule(); |
| + if (setUpFn != null) setUpFn(); |
| + }); |
| + |
| + unittest.tearDown(() { |
| + _currentSchedule = null; |
| + }); |
| +} |
| + |
| +/// Ensures that the global configuration for `scheduled_test` has been |
| +/// initialized. |
| +void _ensureInitialized() { |
| + unittest.ensureInitialized(); |
| + unittest.wrapAsync = (f) { |
| + if (currentSchedule == null) { |
| + 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.
|
| + } |
| + |
| + return currentSchedule.wrapAsync(f); |
| + }; |
| +} |