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")); }); }); }); }
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)); }); // ... }
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))); }); }); }
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] or [wrapFuture].
[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')); }); }
[wrapFuture] works similarly to [wrapAsync], but instead of wrapping a single callback it wraps a whole [Future] chain. Like [wrapAsync], it ensures that the task queue doesn't complete until the out-of-band chain has finished, and that any errors in the chain are piped back into the scheduled test. For example:
import 'package:scheduled_test/scheduled_test.dart'; void main() { test('sendRequest sends a request', () { wrapFuture(server.nextRequest.then((request) { expect(request.body, equals('payload')); expect(request.headers['content-type'], equals('text/plain')); })); schedule(() => sendRequest('payload')); }); }
scheduled_test
has a built-in timeout of 5 seconds (configurable via [Schedule.timeout]). This timeout is aware of the structure of the schedule; this means that it will reset for each task in a queue, when moving between queues, or almost any other sort of interaction with [currentSchedule]. As long as the [Schedule] knows your test is making some sort of progress, it won't time out.
If a single task might take a long time, you can also manually tell the [Schedule] that it‘s making progress by calling [Schedule.heartbeat], which will reset the timeout whenever it’s called.