Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(72)

Unified Diff: pkg/scheduled_test/lib/scheduled_test.dart

Issue 12209073: Add a scheduled test library. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Make everything play nicely with the outside world. Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..23a060ff052cfc5764980f6603abda3411a80eaa
--- /dev/null
+++ b/pkg/scheduled_test/lib/scheduled_test.dart
@@ -0,0 +1,292 @@
+// 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
+/// `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
+/// [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.
+Schedule get currentSchedule => _currentSchedule;
+Schedule _currentSchedule;
+
+/// The user-provided setUp function. This is set for each test during
+/// `unittest.setUp`.
+Function _setUpFn;
+
+/// 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) {
+ _ensureInitialized();
+ _ensureSetUpForTopLevel();
+ testFn(description, () {
+ var asyncDone = unittest.expectAsync0(() {});
+ return currentSchedule.run(() {
+ if (_setUpFn != null) _setUpFn();
+ 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. If
+/// [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 new StateError('There seems to be another scheduled test '
+ 'still running.');
+ }
+ _currentSchedule = new Schedule();
+ _setUpFn = 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 new StateError("Unexpected call to wrapAsync with no current "
+ "schedule.");
+ }
+
+ return currentSchedule.wrapAsync(f);
+ };
+}
« pkg/pkg.status ('K') | « pkg/pkg.status ('k') | pkg/scheduled_test/lib/src/schedule.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698