Add a hooks_testing library (#1952)

Previous test for methods like `expect`, `expectLater`, and
`expectAsync` used internal details of the test runner like `LiveTest`
to check behaviors such as holding the test open and specific failure
behavior.

Add an abstraction `TestCaseMonitor` to hide the implementation details
of creating and running a `LocalTest`. Expose only the parts of
`LiveTest` which are used by current tests - the state and errors.
The new class is defined in a new library `hooks_testing` since it is
intended primarily for writing tests for code that uses the `hooks`
library.
Add a new `State` enum to hide some details of `LiveTest`.
There is already a class named `State` that holds the `Status` and
`Result` for a `LiveTest`. This `State` attempts to encode the useful
parts of the status and result into a single enum. We don't get much use
out of the separate status and result. The `Result` is always `passed`
when the `Status` is anything other than `complete` (it shouldn't be
read), and changing the `Result` to anything else is _always_
accompanied by setting the `status` to `complete`. The new enum also
lets us hide the `failure` and `error` distinction instead of adding new
dependencies to it.
This works towards #1465

Migrate tests under `test_api/test/frontend` for the tests which will be
migrating to `package:matcher` to use the new APIs. Move to a
`utils_new.dart` file which will be moved along with these tests, and
the old `utils.dart` file will remain for testing the scaffolding APIs.
Do not replace the `expectTestsBlock` utility, the usage of this utility
is not any more clear or readable than spelling out the full behavior in
the test.

Add tests in `checks` for the behavior of holding the test for pending
work, and for unawaited failures.
Add utilities for testing against a `TestMonitor` locally within the
test.
diff --git a/pkgs/checks/pubspec.yaml b/pkgs/checks/pubspec.yaml
index 2c35809..6cf9a7a 100644
--- a/pkgs/checks/pubspec.yaml
+++ b/pkgs/checks/pubspec.yaml
@@ -11,7 +11,7 @@
 dependencies:
   async: ^2.8.0
   meta: ^1.9.0
-  test_api: ^0.4.0
+  test_api: ^0.5.0
 
 dev_dependencies:
   test: ^1.21.3
diff --git a/pkgs/checks/pubspec_overrides.yaml b/pkgs/checks/pubspec_overrides.yaml
new file mode 100644
index 0000000..7d9aba4
--- /dev/null
+++ b/pkgs/checks/pubspec_overrides.yaml
@@ -0,0 +1,7 @@
+dependency_overrides:
+  test_api:
+    path: ../test_api
+  test_core:
+    path: ../test_core
+  test:
+    path: ../test
diff --git a/pkgs/checks/test/context_test.dart b/pkgs/checks/test/context_test.dart
new file mode 100644
index 0000000..3fd92ac
--- /dev/null
+++ b/pkgs/checks/test/context_test.dart
@@ -0,0 +1,176 @@
+// Copyright (c) 2023, 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart' hide Result;
+import 'package:checks/checks.dart';
+import 'package:checks/context.dart';
+import 'package:test/scaffolding.dart';
+import 'package:test_api/hooks.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import 'test_shared.dart';
+
+void main() {
+  group('Context', () {
+    test('expectAsync holds test open', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.expectAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          return null;
+        });
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('expectAsync does not hold test open past exception', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.expectAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          throw 'oh no!';
+        });
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.single.has((e) => e.error, 'error').equals('oh no!');
+    });
+
+    test('nestAsync holds test open', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          return Extracted.value(null);
+        }, null);
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('nestAsync holds test open past async condition', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync(() => [''], (actual) async {
+          return Extracted.value(null);
+        }, LazyCondition((it) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+        }));
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('nestAsync does not hold test open past exception', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          throw 'oh no!';
+        }, null);
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.single.has((e) => e.error, 'error').equals('oh no!');
+    });
+
+    test('expectUnawaited can fail the test after it completes', () async {
+      late void Function() callback;
+      final monitor = await TestCaseMonitor.run(() {
+        check(null).context.expectUnawaited(() => [''], (actual, reject) {
+          final completer = Completer<void>()
+            ..future.then((_) {
+              reject(Rejection(which: ['foo']));
+            });
+          callback = completer.complete;
+        });
+      });
+      check(monitor).state.equals(State.passed);
+      callback();
+      await pumpEventQueue();
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.unorderedMatches([
+          it()
+            ..isA<AsyncError>()
+                .has((e) => e.error, 'error')
+                .isA<TestFailure>()
+                .has((f) => f.message, 'message')
+                .isNotNull()
+                .endsWith('Which: foo'),
+          it()
+            ..isA<AsyncError>()
+                .has((e) => e.error, 'error')
+                .isA<String>()
+                .startsWith('This test failed after it had already completed.')
+        ]);
+    });
+  });
+
+  group('SkipExtension', () {
+    test('marks the test as skipped', () async {
+      final monitor = await TestCaseMonitor.run(() {
+        check(null).skip('skip').isNotNull();
+      });
+      check(monitor).state.equals(State.skipped);
+    });
+  });
+}
+
+extension _MonitorChecks on Subject<TestCaseMonitor> {
+  Subject<State> get state => has((m) => m.state, 'state');
+  Subject<Iterable<AsyncError>> get errors => has((m) => m.errors, 'errors');
+  Subject<StreamQueue<AsyncError>> get onError =>
+      has((m) => m.onError, 'onError').withQueue;
+
+  /// Expects that the monitored test is completed as success with no errors.
+  ///
+  /// Sets up an unawaited expectation that the test does not emit errors in the
+  /// future in addition to checking there have been no errors yet.
+  void didPass() {
+    errors.isEmpty();
+    state.equals(State.passed);
+    onError.context.expectUnawaited(() => ['emits no further errors'],
+        (actual, reject) async {
+      await for (var error in actual.rest) {
+        reject(Rejection(which: [
+          ...prefixFirst('threw late error', literal(error.error)),
+          ...(const LineSplitter().convert(
+              TestHandle.current.formatStackTrace(error.stackTrace).toString()))
+        ]));
+      }
+    });
+  }
+}
diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart
index 9d15a05..8c6b257 100644
--- a/pkgs/checks/test/test_shared.dart
+++ b/pkgs/checks/test/test_shared.dart
@@ -59,7 +59,7 @@
         ]);
       }
       return Extracted.value(failure.rejection);
-    }, _LazyCondition((rejection) {
+    }, LazyCondition((rejection) {
       if (didRunCallback) {
         rejection
             .has((r) => r.actual, 'actual')
@@ -96,9 +96,9 @@
 ///
 /// Allows basing the following condition in `isRejectedByAsync` on the actual
 /// value.
-class _LazyCondition<T> implements Condition<T> {
+class LazyCondition<T> implements Condition<T> {
   final FutureOr<void> Function(Subject<T>) _apply;
-  _LazyCondition(this._apply);
+  LazyCondition(this._apply);
 
   @override
   void apply(Subject<T> subject) {
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md
index e3fb57d..0b5dd84 100644
--- a/pkgs/test_api/CHANGELOG.md
+++ b/pkgs/test_api/CHANGELOG.md
@@ -6,6 +6,8 @@
   major release.
 * **BREAKING** Add required `defaultCompiler` and `supportedCompilers` fields
   to `Runtime`.
+* Add `package:test_api/hooks_testing.dart` library for writing tests against
+  code that uses `package:test_api/hooks.dart`.
 
 ## 0.4.18
 
diff --git a/pkgs/test_api/lib/hooks_testing.dart b/pkgs/test_api/lib/hooks_testing.dart
new file mode 100644
index 0000000..8e4a1e9
--- /dev/null
+++ b/pkgs/test_api/lib/hooks_testing.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2023, 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.
+
+import 'dart:async';
+
+import 'src/backend/group.dart';
+import 'src/backend/invoker.dart';
+import 'src/backend/live_test.dart';
+import 'src/backend/metadata.dart';
+import 'src/backend/runtime.dart';
+import 'src/backend/state.dart';
+import 'src/backend/suite.dart';
+import 'src/backend/suite_platform.dart';
+
+export 'src/backend/state.dart' show Result, Status;
+export 'src/backend/test_failure.dart' show TestFailure;
+
+/// A monitor for the behavior of a callback when it is run as the body of a
+/// test case.
+///
+/// Allows running a callback as the body of a local test case and querying for
+/// the current [state], and [errors], and subscribing to future errors.
+///
+/// Use [run] to run a test body and query for the success or failure.
+///
+/// Use [start] to start a test and query for whether it has finished running.
+class TestCaseMonitor {
+  final LiveTest _liveTest;
+  final _done = Completer<void>();
+  TestCaseMonitor._(FutureOr<void> Function() body)
+      : _liveTest = _createTest(body);
+
+  /// Run [body] as a test case and return a [TestCaseMonitor] with the result.
+  ///
+  /// The [state] will either [State.passed], [State.skipped], or
+  /// [State.failed], the test will no longer be running.
+  ///
+  /// {@template result-late-fail}
+  /// Note that a test can change state from [State.passed] to [State.failed]
+  /// if the test surfaces an unawaited asynchronous error.
+  /// {@endtemplate}
+  ///
+  /// ```dart
+  /// final monitor = await TestCaseMonitor.run(() {
+  ///   fail('oh no!');
+  /// });
+  /// assert(monitor.state == State.failed);
+  /// assert((monitor.errors.single.error as TestFailure).message == 'oh no!');
+  /// ```
+  static Future<TestCaseMonitor> run(FutureOr<void> Function() body) async {
+    final monitor = TestCaseMonitor.start(body);
+    await monitor.onDone;
+    return monitor;
+  }
+
+  /// Start [body] as a test case and return a [TestCaseMonitor] with the status
+  /// and result.
+  ///
+  /// The [state] will start as [State.pending] if queried synchronously, but it
+  /// will switch to [State.running]. After `onDone` completes the state will be
+  /// one of [State.passed], [State.skipped], or [State.failed].
+  ///
+  /// {@macro result-late-fail}
+  ///
+  /// ```dart
+  /// late void Function() completeWork;
+  /// final monitor = TestCaseMonitor.start(() {
+  ///   final outstandingWork = TestHandle.current.markPending();
+  ///   completeWork = outstandingWork.complete;
+  /// });
+  /// await pumpEventQueue();
+  /// assert(monitor.state == State.running);
+  /// completeWork();
+  /// await monitor.onDone;
+  /// assert(monitor.state == State.passed);
+  /// ```
+  static TestCaseMonitor start(FutureOr<void> Function() body) =>
+      TestCaseMonitor._(body).._start();
+
+  void _start() {
+    _liveTest.run().whenComplete(_done.complete);
+  }
+
+  /// A future that completes after this test has finished running, or has
+  /// surfaced an error.
+  Future<void> get onDone => _done.future;
+
+  /// The running and success or failure status for the test case.
+  State get state {
+    final status = _liveTest.state.status;
+    if (status == Status.pending) return State.pending;
+    if (status == Status.running) return State.running;
+    final result = _liveTest.state.result;
+    if (result == Result.skipped) return State.skipped;
+    if (result == Result.success) return State.passed;
+    // result == Result.failure || result == Result.error
+    return State.failed;
+  }
+
+  /// The errors surfaced by the test.
+  ///
+  /// A test with any errors will have a [state] of [State.failed].
+  ///
+  /// {@macro result-late-fail}
+  ///
+  /// A test may have more than one error if there were unhandled asynchronous
+  /// errors surfaced after the test is done.
+  Iterable<AsyncError> get errors => _liveTest.errors;
+
+  /// A stream of errors surfaced by the test.
+  ///
+  /// This stream will not close, asynchronous errors may be surfaced within the
+  /// test's error zone at any point.
+  Stream<AsyncError> get onError => _liveTest.onError;
+}
+
+/// Returns a local [LiveTest] that runs [body].
+LiveTest _createTest(FutureOr<void> Function() body) {
+  var test = LocalTest('test', Metadata(chainStackTraces: true), body);
+  var suite = Suite(Group.root([test]), _suitePlatform, ignoreTimeouts: false);
+  return test.load(suite);
+}
+
+/// A dummy suite platform to use for testing suites.
+final _suitePlatform =
+    SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler);
+
+/// The running and success state of a test monitored by a [TestCaseMonitor].
+enum State {
+  /// The test is has not yet started.
+  pending,
+
+  /// The test is running and has not yet failed.
+  running,
+
+  /// The test has completed without any error.
+  ///
+  /// This implies that the test body has completed, and no error has surfaced
+  /// *yet*. However, it this doesn't mean that the test won't fail in the
+  /// future.
+  passed,
+
+  /// The test, or some part of it, has been skipped.
+  ///
+  /// This does not imply that the test has not had an error, but if there are
+  /// errors they are ignored.
+  skipped,
+
+  /// The test has failed.
+  ///
+  /// An test fails when any exception, typically a [TestFailure], is thrown in
+  /// the test's zone. A test that has failed may still have additional errors
+  /// that surface as unhandled asynchronous errors.
+  failed,
+}
diff --git a/pkgs/test_api/test/frontend/expect_async_test.dart b/pkgs/test_api/test/frontend/expect_async_test.dart
index dde2e41..3752b7b 100644
--- a/pkgs/test_api/test/frontend/expect_async_test.dart
+++ b/pkgs/test_api/test/frontend/expect_async_test.dart
@@ -6,41 +6,40 @@
 
 import 'package:fake_async/fake_async.dart';
 import 'package:test/test.dart';
-import 'package:test_api/src/backend/live_test.dart';
-import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../utils.dart';
+import '../utils_new.dart';
 
 void main() {
   group('supports a function with this many arguments:', () {
     test('0', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync0(() {
           callbackRun = true;
         })();
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('1', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync1((int arg) {
           expect(arg, equals(1));
           callbackRun = true;
         })(1);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('2', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync2((arg1, arg2) {
           expect(arg1, equals(1));
           expect(arg2, equals(2));
@@ -48,13 +47,13 @@
         })(1, 2);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('3', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync3((arg1, arg2, arg3) {
           expect(arg1, equals(1));
           expect(arg2, equals(2));
@@ -63,13 +62,13 @@
         })(1, 2, 3);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('4', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync4((arg1, arg2, arg3, arg4) {
           expect(arg1, equals(1));
           expect(arg2, equals(2));
@@ -79,13 +78,13 @@
         })(1, 2, 3, 4);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('5', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync5((arg1, arg2, arg3, arg4, arg5) {
           expect(arg1, equals(1));
           expect(arg2, equals(2));
@@ -96,13 +95,13 @@
         })(1, 2, 3, 4, 5);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('6', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync6((arg1, arg2, arg3, arg4, arg5, arg6) {
           expect(arg1, equals(1));
           expect(arg2, equals(2));
@@ -114,7 +113,7 @@
         })(1, 2, 3, 4, 5, 6);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
   });
@@ -122,46 +121,55 @@
   group('with optional arguments', () {
     test('allows them to be passed', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync1(([arg = 1]) {
           expect(arg, equals(2));
           callbackRun = true;
         })(2);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('allows them not to be passed', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync1(([arg = 1]) {
           expect(arg, equals(1));
           callbackRun = true;
         })();
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
   });
 
   group('by default', () {
-    test("won't allow the test to complete until it's called", () {
-      return expectTestBlocks(
-          () => expectAsync0(() {}), (callback) => callback());
+    test("won't allow the test to complete until it's called", () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {});
+      });
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
     });
 
     test('may only be called once', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var callback = expectAsync0(() {});
         callback();
         callback();
       });
 
       expectTestFailed(
-          liveTest, 'Callback called more times than expected (1).');
+          monitor, 'Callback called more times than expected (1).');
     });
   });
 
@@ -169,36 +177,31 @@
     test(
         "won't allow the test to complete until it's called at least that "
         'many times', () async {
-      late LiveTest liveTest;
-      late Future future;
-      liveTest = createTest(() {
-        var callback = expectAsync0(() {}, count: 3);
-
-        future = () async {
-          await pumpEventQueue();
-          expect(liveTest.state.status, equals(Status.running));
-          callback();
-
-          await pumpEventQueue();
-          expect(liveTest.state.status, equals(Status.running));
-          callback();
-
-          await pumpEventQueue();
-          expect(liveTest.state.status, equals(Status.running));
-          callback();
-        }();
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {}, count: 3);
       });
 
-      await liveTest.run();
-      expectTestPassed(liveTest);
-      // Ensure that the outer test doesn't complete until the inner future
-      // completes.
-      await future;
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
     });
 
     test("will throw an error if it's called more than that many times",
         () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var callback = expectAsync0(() {}, count: 3);
         callback();
         callback();
@@ -207,7 +210,7 @@
       });
 
       expectTestFailed(
-          liveTest, 'Callback called more times than expected (3).');
+          monitor, 'Callback called more times than expected (3).');
     });
 
     group('0,', () {
@@ -216,12 +219,12 @@
       });
 
       test("will throw an error if it's ever called", () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expectAsync0(() {}, count: 0)();
         });
 
         expectTestFailed(
-            liveTest, 'Callback called more times than expected (0).');
+            monitor, 'Callback called more times than expected (0).');
       });
     });
   });
@@ -241,7 +244,7 @@
 
     test("will throw an error if it's called more than that many times",
         () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var callback = expectAsync0(() {}, max: 3);
         callback();
         callback();
@@ -250,7 +253,7 @@
       });
 
       expectTestFailed(
-          liveTest, 'Callback called more times than expected (3).');
+          monitor, 'Callback called more times than expected (3).');
     });
 
     test('-1, will allow the callback to be called any number of times', () {
@@ -268,25 +271,25 @@
   group('expectAsyncUntil()', () {
     test("won't allow the test to complete until isDone returns true",
         () async {
-      late LiveTest liveTest;
+      late TestCaseMonitor monitor;
       late Future future;
-      liveTest = createTest(() {
+      monitor = TestCaseMonitor.start(() {
         var done = false;
         var callback = expectAsyncUntil0(() {}, () => done);
 
         future = () async {
           await pumpEventQueue();
-          expect(liveTest.state.status, equals(Status.running));
+          expect(monitor.state, equals(State.running));
           callback();
           await pumpEventQueue();
-          expect(liveTest.state.status, equals(Status.running));
+          expect(monitor.state, equals(State.running));
           done = true;
           callback();
         }();
       });
+      await monitor.onDone;
 
-      await liveTest.run();
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       // Ensure that the outer test doesn't complete until the inner future
       // completes.
       await future;
@@ -302,77 +305,77 @@
   });
 
   test('allows errors', () async {
-    var liveTest = await runTestBody(() {
+    var monitor = await TestCaseMonitor.run(() {
       expect(expectAsync0(() => throw 'oh no'), throwsA('oh no'));
     });
 
-    expectTestPassed(liveTest);
+    expectTestPassed(monitor);
   });
 
   test('may be called in a non-test zone', () async {
-    var liveTest = await runTestBody(() {
+    var monitor = await TestCaseMonitor.run(() {
       var callback = expectAsync0(() {});
       Zone.root.run(callback);
     });
-    expectTestPassed(liveTest);
+    expectTestPassed(monitor);
   });
 
   test('may be called in a FakeAsync zone that does not run further', () async {
-    var liveTest = await runTestBody(() {
+    var monitor = await TestCaseMonitor.run(() {
       FakeAsync().run((_) {
         var callback = expectAsync0(() {});
         callback();
       });
     });
-    expectTestPassed(liveTest);
+    expectTestPassed(monitor);
   });
 
   group('old-style expectAsync()', () {
     test('works with no arguments', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync(() {
           callbackRun = true;
         })();
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('works with dynamic arguments', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync((arg1, arg2) {
           callbackRun = true;
         })(1, 2);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('works with non-nullable arguments', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync((int arg1, int arg2) {
           callbackRun = true;
         })(1, 2);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
     test('works with 6 arguments', () async {
       var callbackRun = false;
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) {
           callbackRun = true;
         })(1, 2, 3, 4, 5, 6);
       });
 
-      expectTestPassed(liveTest);
+      expectTestPassed(monitor);
       expect(callbackRun, isTrue);
     });
 
diff --git a/pkgs/test_api/test/frontend/expect_test.dart b/pkgs/test_api/test/frontend/expect_test.dart
index ee8d243..cae3511 100644
--- a/pkgs/test_api/test/frontend/expect_test.dart
+++ b/pkgs/test_api/test/frontend/expect_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:test/test.dart';
 
-import '../utils.dart';
+import '../utils_new.dart';
 
 void main() {
   group('returned Future from expectLater()', () {
diff --git a/pkgs/test_api/test/frontend/matcher/completion_test.dart b/pkgs/test_api/test/frontend/matcher/completion_test.dart
index 1aee139..4f114af 100644
--- a/pkgs/test_api/test/frontend/matcher/completion_test.dart
+++ b/pkgs/test_api/test/frontend/matcher/completion_test.dart
@@ -5,18 +5,18 @@
 import 'dart:async';
 
 import 'package:test/test.dart';
-import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../../utils.dart';
+import '../../utils_new.dart';
 
 void main() {
   group('[doesNotComplete]', () {
     test('fails when provided a non future', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(10, doesNotComplete);
       });
 
-      expectTestFailed(liveTest, contains('10 is not a Future'));
+      expectTestFailed(monitor, contains('10 is not a Future'));
     });
 
     test('succeeds when a future does not complete', () {
@@ -25,33 +25,33 @@
     });
 
     test('fails when a future does complete', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var completer = Completer();
         completer.complete(null);
         expect(completer.future, doesNotComplete);
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Future was not expected to complete but completed with a value of'
           ' null');
     });
 
     test('fails when a future completes after the expect', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var completer = Completer();
         expect(completer.future, doesNotComplete);
         completer.complete(null);
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Future was not expected to complete but completed with a value of'
           ' null');
     });
 
     test('fails when a future eventually completes', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         var completer = Completer();
         expect(completer.future, doesNotComplete);
         Future(() async {
@@ -60,46 +60,48 @@
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Future was not expected to complete but completed with a value of'
           ' null');
     });
   });
   group('[completes]', () {
-    test('blocks the test until the Future completes', () {
-      return expectTestBlocks(() {
-        var completer = Completer();
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
         expect(completer.future, completes);
-        return completer;
-      }, (completer) => completer.complete());
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
     });
 
     test('with an error', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(Future.error('X'), completes);
       });
 
-      expect(liveTest.state.status, equals(Status.complete));
-      expect(liveTest.state.result, equals(Result.error));
-      expect(liveTest.errors, hasLength(1));
-      expect(liveTest.errors.first.error, equals('X'));
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
     });
 
     test('with a failure', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(Future.error(TestFailure('oh no')), completes);
       });
 
-      expectTestFailed(liveTest, 'oh no');
+      expectTestFailed(monitor, 'oh no');
     });
 
     test('with a non-future', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(10, completes);
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Expected: completes successfully\n'
           '  Actual: <10>\n'
           '   Which: was not a Future\n');
@@ -111,52 +113,54 @@
   });
 
   group('[completion]', () {
-    test('blocks the test until the Future completes', () {
-      return expectTestBlocks(() {
-        var completer = Completer();
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<Object?>();
+      final monitor = TestCaseMonitor.start(() {
         expect(completer.future, completion(isNull));
-        return completer;
-      }, (completer) => completer.complete());
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete(null);
+      await monitor.onDone;
+      expectTestPassed(monitor);
     });
 
     test('with an error', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(Future.error('X'), completion(isNull));
       });
 
-      expect(liveTest.state.status, equals(Status.complete));
-      expect(liveTest.state.result, equals(Result.error));
-      expect(liveTest.errors, hasLength(1));
-      expect(liveTest.errors.first.error, equals('X'));
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
     });
 
     test('with a failure', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(Future.error(TestFailure('oh no')), completion(isNull));
       });
 
-      expectTestFailed(liveTest, 'oh no');
+      expectTestFailed(monitor, 'oh no');
     });
 
     test('with a non-future', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(10, completion(equals(10)));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Expected: completes to a value that <10>\n'
           '  Actual: <10>\n'
           '   Which: was not a Future\n');
     });
 
     test('with an incorrect value', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(Future.value('a'), completion(equals('b')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: completes to a value that 'b'\n"
                 '  Actual: <'),
diff --git a/pkgs/test_api/test/frontend/matcher/prints_test.dart b/pkgs/test_api/test/frontend/matcher/prints_test.dart
index 92ab6a7..337a812 100644
--- a/pkgs/test_api/test/frontend/matcher/prints_test.dart
+++ b/pkgs/test_api/test/frontend/matcher/prints_test.dart
@@ -5,8 +5,9 @@
 import 'dart:async';
 
 import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../../utils.dart';
+import '../../utils_new.dart';
 
 void main() {
   group('synchronous', () {
@@ -27,12 +28,12 @@
 
     test('describes a failure nicely', () async {
       void local() => print('Hello, world!');
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints('Goodbye, world!\n'));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints 'Goodbye, world!\\n'\n"
                 "            ''\n"
@@ -50,12 +51,12 @@
 
     test('describes a failure with a non-descriptive Matcher nicely', () async {
       void local() => print('Hello, world!');
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints(contains('Goodbye')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints contains 'Goodbye'\n"
                 '  Actual: <'),
@@ -67,12 +68,12 @@
 
     test('describes a failure with no text nicely', () async {
       void local() {}
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints(contains('Goodbye')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints contains 'Goodbye'\n"
                 '  Actual: <'),
@@ -82,12 +83,12 @@
     });
 
     test('with a non-function', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(10, prints(contains('Goodbye')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           "Expected: prints contains 'Goodbye'\n"
           '  Actual: <10>\n'
           '   Which: was not a unary Function\n');
@@ -116,12 +117,12 @@
 
     test('describes a failure nicely', () async {
       void local() => Future(() => print('Hello, world!'));
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints('Goodbye, world!\n'));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints 'Goodbye, world!\\n'\n"
                 "            ''\n"
@@ -139,12 +140,12 @@
 
     test('describes a failure with a non-descriptive Matcher nicely', () async {
       void local() => Future(() => print('Hello, world!'));
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints(contains('Goodbye')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints contains 'Goodbye'\n"
                 '  Actual: <'),
@@ -156,12 +157,12 @@
 
     test('describes a failure with no text nicely', () async {
       void local() => Future.value();
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         expect(local, prints(contains('Goodbye')));
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           allOf([
             startsWith("Expected: prints contains 'Goodbye'\n"
                 '  Actual: <'),
@@ -170,12 +171,16 @@
           ]));
     });
 
-    test("won't let the test end until the Future completes", () {
-      return expectTestBlocks(() {
-        var completer = Completer();
+    test("won't let the test end until the Future completes", () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
         expect(() => completer.future, prints(isEmpty));
-        return completer;
-      }, (completer) => completer.complete());
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
     });
 
     test("blocks expectLater's Future", () async {
diff --git a/pkgs/test_api/test/frontend/matcher/throws_test.dart b/pkgs/test_api/test/frontend/matcher/throws_test.dart
index 40b2be6..a9e76af 100644
--- a/pkgs/test_api/test/frontend/matcher/throws_test.dart
+++ b/pkgs/test_api/test/frontend/matcher/throws_test.dart
@@ -5,8 +5,9 @@
 import 'dart:async';
 
 import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../../utils.dart';
+import '../../utils_new.dart';
 
 void main() {
   group('synchronous', () {
@@ -17,12 +18,12 @@
 
       test("with a function that doesn't throw", () async {
         void local() {}
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(local, throws);
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith('Expected: throws\n'
                   '  Actual: <'),
@@ -32,12 +33,12 @@
       });
 
       test('with a non-function', () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(10, throws);
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             'Expected: throws\n'
             '  Actual: <10>\n'
             '   Which: was not a Function or Future\n');
@@ -55,12 +56,12 @@
 
       test("with a function that doesn't throw", () async {
         void local() {}
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(local, throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -70,24 +71,24 @@
       });
 
       test('with a non-function', () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(10, throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             "Expected: throws 'oh no'\n"
             '  Actual: <10>\n'
             '   Which: was not a Function or Future\n');
       });
 
       test('with a function that throws the wrong error', () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(() => throw 'aw dang', throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -111,12 +112,12 @@
       });
 
       test("with a Future that doesn't throw", () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(Future.value(), throws);
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith('Expected: throws\n'
                   '  Actual: <'),
@@ -130,12 +131,12 @@
       });
 
       test("with a closure that returns a Future that doesn't throw", () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(() => Future.value(), throws);
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith('Expected: throws\n'
                   '  Actual: <'),
@@ -144,12 +145,18 @@
             ]));
       });
 
-      test("won't let the test end until the Future completes", () {
-        return expectTestBlocks(() {
-          var completer = Completer();
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
           expect(completer.future, throws);
-          return completer;
-        }, (completer) => completer.completeError('oh no'));
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+        expectTestPassed(monitor);
       });
     });
 
@@ -164,12 +171,12 @@
       });
 
       test("with a Future that doesn't throw", () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(Future.value(), throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -179,12 +186,12 @@
       });
 
       test('with a Future that throws the wrong error', () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(Future.error('aw dang'), throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -200,12 +207,12 @@
       });
 
       test("with a closure that returns a Future that doesn't throw", () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(() => Future.value(), throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -216,12 +223,12 @@
 
       test('with closure that returns a Future that throws the wrong error',
           () async {
-        var liveTest = await runTestBody(() {
+        var monitor = await TestCaseMonitor.run(() {
           expect(() => Future.error('aw dang'), throwsA('oh no'));
         });
 
         expectTestFailed(
-            liveTest,
+            monitor,
             allOf([
               startsWith("Expected: throws 'oh no'\n"
                   '  Actual: <'),
@@ -230,12 +237,19 @@
             ]));
       });
 
-      test("won't let the test end until the Future completes", () {
-        return expectTestBlocks(() {
-          var completer = Completer();
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
           expect(completer.future, throwsA('oh no'));
-          return completer;
-        }, (completer) => completer.completeError('oh no'));
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+
+        expectTestPassed(monitor);
       });
 
       test("blocks expectLater's Future", () async {
diff --git a/pkgs/test_api/test/frontend/matcher/throws_type_test.dart b/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
index 6e0a482..dfad890 100644
--- a/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
+++ b/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
@@ -3,8 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../../utils.dart';
+import '../../utils_new.dart';
 
 void main() {
   group('[throwsArgumentError]', () {
@@ -13,7 +14,7 @@
     });
 
     test('fails when a non-ArgumentError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsArgumentError);
       });
 
@@ -29,7 +30,7 @@
     });
 
     test('fails when a non-ConcurrentModificationError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsConcurrentModificationError);
       });
 
@@ -49,7 +50,7 @@
     });
 
     test('fails when a non-CyclicInitializationError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsCyclicInitializationError);
       });
 
@@ -64,7 +65,7 @@
     });
 
     test('fails when a non-Exception is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw 'oh no', throwsException);
       });
 
@@ -79,7 +80,7 @@
     });
 
     test('fails when a non-FormatException is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsFormatException);
       });
 
@@ -96,7 +97,7 @@
     });
 
     test('fails when a non-NoSuchMethodError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsNoSuchMethodError);
       });
 
@@ -111,7 +112,7 @@
     });
 
     test('fails when a non-RangeError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsRangeError);
       });
 
@@ -126,7 +127,7 @@
     });
 
     test('fails when a non-StateError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsStateError);
       });
 
@@ -141,7 +142,7 @@
     });
 
     test('fails when a non-UnimplementedError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsUnimplementedError);
       });
 
@@ -156,7 +157,7 @@
     });
 
     test('fails when a non-UnsupportedError is thrown', () async {
-      var liveTest = await runTestBody(() {
+      var liveTest = await TestCaseMonitor.run(() {
         expect(() => throw Exception(), throwsUnsupportedError);
       });
 
diff --git a/pkgs/test_api/test/frontend/never_called_test.dart b/pkgs/test_api/test/frontend/never_called_test.dart
index e9170f9..f4160a3 100644
--- a/pkgs/test_api/test/frontend/never_called_test.dart
+++ b/pkgs/test_api/test/frontend/never_called_test.dart
@@ -4,8 +4,9 @@
 
 import 'package:term_glyph/term_glyph.dart' as glyph;
 import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
 
-import '../utils.dart';
+import '../utils_new.dart';
 
 void main() {
   setUpAll(() {
@@ -13,32 +14,32 @@
   });
 
   test("doesn't throw if it isn't called", () async {
-    var liveTest = await runTestBody(() {
+    var monitor = await TestCaseMonitor.run(() {
       const Stream.empty().listen(neverCalled);
     });
 
-    expectTestPassed(liveTest);
+    expectTestPassed(monitor);
   });
 
   group("if it's called", () {
     test('throws', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         neverCalled();
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Callback should never have been called, but it was called with no '
           'arguments.');
     });
 
     test('pretty-prints arguments', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         neverCalled(1, 'foo\nbar');
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Callback should never have been called, but it was called with:\n'
           '* <1>\n'
           "* 'foo\\n'\n"
@@ -46,18 +47,18 @@
     });
 
     test('keeps the test alive', () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         pumpEventQueue(times: 10).then(neverCalled);
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Callback should never have been called, but it was called with:\n'
           '* <null>');
     });
 
     test("can't be caught", () async {
-      var liveTest = await runTestBody(() {
+      var monitor = await TestCaseMonitor.run(() {
         try {
           neverCalled();
         } catch (_) {
@@ -66,7 +67,7 @@
       });
 
       expectTestFailed(
-          liveTest,
+          monitor,
           'Callback should never have been called, but it was called with '
           'no arguments.');
     });
diff --git a/pkgs/test_api/test/frontend/stream_matcher_test.dart b/pkgs/test_api/test/frontend/stream_matcher_test.dart
index deb21fb..37d37bf 100644
--- a/pkgs/test_api/test/frontend/stream_matcher_test.dart
+++ b/pkgs/test_api/test/frontend/stream_matcher_test.dart
@@ -8,7 +8,7 @@
 import 'package:term_glyph/term_glyph.dart' as glyph;
 import 'package:test/test.dart';
 
-import '../utils.dart';
+import '../utils_new.dart';
 
 void main() {
   setUpAll(() {
diff --git a/pkgs/test_api/test/utils.dart b/pkgs/test_api/test/utils.dart
index c8c4741..ca3d729 100644
--- a/pkgs/test_api/test/utils.dart
+++ b/pkgs/test_api/test/utils.dart
@@ -6,14 +6,10 @@
 
 import 'package:test/test.dart';
 import 'package:test_api/src/backend/declarer.dart';
-import 'package:test_api/src/backend/group.dart';
 import 'package:test_api/src/backend/group_entry.dart';
-import 'package:test_api/src/backend/invoker.dart';
 import 'package:test_api/src/backend/live_test.dart';
-import 'package:test_api/src/backend/metadata.dart';
 import 'package:test_api/src/backend/runtime.dart';
 import 'package:test_api/src/backend/state.dart';
-import 'package:test_api/src/backend/suite.dart';
 import 'package:test_api/src/backend/suite_platform.dart';
 import 'package:test_core/src/runner/engine.dart';
 import 'package:test_core/src/runner/plugin/environment.dart';
@@ -77,34 +73,12 @@
   ]);
 }
 
-/// Returns a matcher that matches a callback or Future that throws a
-/// [TestFailure] with the given [message].
-///
-/// [message] can be a string or a [Matcher].
-Matcher throwsTestFailure(message) => throwsA(isTestFailure(message));
-
 /// Returns a matcher that matches a [TestFailure] with the given [message].
 ///
 /// [message] can be a string or a [Matcher].
 Matcher isTestFailure(message) => const TypeMatcher<TestFailure>()
     .having((e) => e.message, 'message', message);
 
-/// Returns a local [LiveTest] that runs [body].
-LiveTest createTest(dynamic Function() body) {
-  var test = LocalTest('test', Metadata(chainStackTraces: true), body);
-  var suite = Suite(Group.root([test]), suitePlatform, ignoreTimeouts: false);
-  return test.load(suite);
-}
-
-/// Runs [body] as a test.
-///
-/// Once it completes, returns the [LiveTest] used to run it.
-Future<LiveTest> runTestBody(dynamic Function() body) async {
-  var liveTest = createTest(body);
-  await liveTest.run();
-  return liveTest;
-}
-
 /// Asserts that [liveTest] has completed and passed.
 ///
 /// If the test had any errors, they're surfaced nicely into the outer test.
@@ -131,29 +105,6 @@
   expect(liveTest.errors.first.error, isTestFailure(message));
 }
 
-/// Assert that the [test] callback causes a test to block until [stopBlocking]
-/// is called at some later time.
-///
-/// [stopBlocking] is passed the return value of [test].
-Future expectTestBlocks(
-    dynamic Function() test, dynamic Function(dynamic) stopBlocking) async {
-  late LiveTest liveTest;
-  late Future future;
-  liveTest = createTest(() {
-    var value = test();
-    future = pumpEventQueue().then((_) {
-      expect(liveTest.state.status, equals(Status.running));
-      stopBlocking(value);
-    });
-  });
-
-  await liveTest.run();
-  expectTestPassed(liveTest);
-  // Ensure that the outer test doesn't complete until the inner future
-  // completes.
-  return future;
-}
-
 /// Runs [body] with a declarer, runs all the declared tests, and asserts that
 /// they pass.
 ///
diff --git a/pkgs/test_api/test/utils_new.dart b/pkgs/test_api/test/utils_new.dart
new file mode 100644
index 0000000..5aa6974
--- /dev/null
+++ b/pkgs/test_api/test/utils_new.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, 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.
+
+import 'dart:async';
+
+import 'package:test_api/expect.dart';
+import 'package:test_api/hooks_testing.dart';
+
+/// Asserts that [liveTest] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(TestCaseMonitor monitor) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the running test, because they're definitely unexpected and it is most
+  // useful for the error to point directly to the throw point.
+  for (var error in monitor.errors) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  }
+  monitor.onError.listen((error) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  });
+
+  expect(monitor.state, State.passed);
+}
+
+/// Asserts that [liveTest] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(TestCaseMonitor monitor, message) {
+  expect(monitor.state, State.failed);
+  expect(monitor.errors, [isAsyncError(isTestFailure(message))]);
+}
+
+/// Returns a matcher that matches a [AsyncError] with an `error` field matching
+/// [errorMatcher].
+Matcher isAsyncError(Matcher errorMatcher) =>
+    isA<AsyncError>().having((e) => e.error, 'error', errorMatcher);
+
+/// Returns a matcher that matches a [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher isTestFailure(message) => const TypeMatcher<TestFailure>()
+    .having((e) => e.message, 'message', message);
+
+/// Returns a matcher that matches a callback or Future that throws a
+/// [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher throwsTestFailure(message) => throwsA(isTestFailure(message));