Observing and testing
Observer
Section titled “Observer”An outcome describes one job’s result. A JobObserver also receives
lifecycle events, logs and errors from work outside the body:
final class Log extends JobObserver { @override void onFinish(Job<Object?> job) => print('$job: ${job.outcome}');
@override void onLog(Job<Object?> job, Object? message) { final data = message is Object? Function() ? message() : message; print('$job: $data'); }}
final job = Job<int>( key: 'load', observer: Log(), (ctx) => ctx.wait(load),); // Job(load): Done(3)It has four hooks: onStart, onFinish, onError and onLog. Job calls
the first three automatically; the body sends messages to onLog through
ctx.log(message).
All four have empty default implementations, so you can override only
those you need. You can also use implements JobObserver if your class
already extends another class. Pass the observer when creating the job;
children inherit it unless they have their own. If a hook throws, its
error goes to the current zone without changing the job’s behavior.
A job’s string representation is Job($key). Use key to identify it in
logs or in a library’s scheduling rules, such as solo queue policies.
describe adds context to the log description.
Messages passed to ctx.log remain objects until the listener formats
them. With no observer, ctx.log does nothing, but Dart still evaluates
its argument. For example, ctx.log('migration failed: $error') formats
the string even without an observer. Pass the object directly to leave
formatting to the listener, or pass a callback to defer building the
message itself:
ctx.log(() => 'migration failed: $error');The observer above calls the callback and formats the returned data.
Without an observer, the callback is never called and the interpolation
does not run. This is a convention of this observer: ctx.log passes the
callback through unchanged, just like any other object.
A body error is sent to the observer. If the job ends with that error,
it is stored in Failed and is also reported to the job’s creation zone
if the outcome remains unobserved.
Errors outside the body cannot become its outcome. These include late
errors from an action abandoned by wait, cleanup errors, cancellation
callback errors (ctx.onCancel or job.whenCancelled), errors from
ctx.unattended and errors while formatting a child’s cancellation
description. They go to the observer, or directly to the job’s creation
zone if there is no observer. An observer decides how to handle them.
A Cancelled reported through this route goes only to the observer and
is never forwarded to the zone as an unhandled error.
Testing
Section titled “Testing”Testing start, cancellation and cleanup requires controlling microtasks
and timers. package:fake_async lets you do this without waiting in real
time. This package uses it in its own tests; here it checks that cancelling
a database open still closes the database once opening finishes:
test('a cancelled open still closes what it opened', () { fakeAsync((async) { var closed = 0; final job = Job<Database>( (ctx) => ctx.join( Database.open, discard: (db) { closed++; return db.close(); }, ), );
async.elapse(const Duration(milliseconds: 10)); job.cancel().ignore(); // nothing awaits inside `fakeAsync` async.flushTimers();
expect(job.outcome, isA<Cancelled>()); expect(closed, 1); // what the test is named for });});flushMicrotasks() is enough to start a job. Operations scheduled through
Future(...) or Future.delayed(...) use timers, so advance them with
flushTimers() or elapse(...).