[audio_core][test] Refactor audio_core integration tests, step 2

The existing TestFixture methods ExpectCallback and ExpectError are
useful shorthands but can produce difficult-to-diagnose error messages
because they don't contain any context about the error. To address this
difficulty, I've replaced the ErrorHandler() and CompletionCallback()
methods with:

* AddErrorHandler, which ties each error handler to a specific FIDL
  protocol object to help diagnose which protocol object failed

* AddCallback, which is like the old CompletionCallback except that
  each callback is named. Additionally, multiple callbacks can be queued
  to test an expected sequence of calls (this simplifies a few tests).

Most of the changes in this CL follow mechanically from the above
changes. The exception is api/gain_control_test.cc, which was also
dramatically simplified using a TYPED_TEST_SUITE.

Bug: 50645
Multiply: audio-core-api-tests
Change-Id: I8bd8d89000f6667ee8e9c5a77e7ab2911a9fabc5
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/401610
Commit-Queue: Tom Bergan <tombergan@google.com>
Reviewed-by: Tim Detwiler <tjdetwiler@google.com>
Testability-Review: Tim Detwiler <tjdetwiler@google.com>
diff --git a/src/media/audio/audio_core/test/api/BUILD.gn b/src/media/audio/audio_core/test/api/BUILD.gn
index 3679212..2791d26 100644
--- a/src/media/audio/audio_core/test/api/BUILD.gn
+++ b/src/media/audio/audio_core/test/api/BUILD.gn
@@ -39,7 +39,6 @@
     "audio_renderer_test.cc",
     "audio_tuner_test.cc",
     "gain_control_test.cc",
-    "gain_control_test.h",
     "usage_gain_reporter_test.cc",
     "usage_reporter_test.cc",
     "volume_control.cc",
diff --git a/src/media/audio/audio_core/test/api/activity_reporter_test.cc b/src/media/audio/audio_core/test/api/activity_reporter_test.cc
index 5c8ded7..d6f20b3 100644
--- a/src/media/audio/audio_core/test/api/activity_reporter_test.cc
+++ b/src/media/audio/audio_core/test/api/activity_reporter_test.cc
@@ -30,11 +30,9 @@
 TEST_F(ActivityReporterTest, ConnectToActivityReporter) {
   fuchsia::media::ActivityReporterPtr activity_reporter;
   environment()->ConnectToService(activity_reporter.NewRequest());
-  activity_reporter.set_error_handler(ErrorHandler());
+  AddErrorHandler(activity_reporter, "ActivityReporter");
 
-  activity_reporter->WatchRenderActivity(
-      CompletionCallback([&](const std::vector<fuchsia::media::AudioRenderUsage>& activity) {}));
-
+  activity_reporter->WatchRenderActivity(AddCallback("WatchRenderActivity"));
   ExpectCallback();
 }
 
diff --git a/src/media/audio/audio_core/test/api/audio_capturer_test.cc b/src/media/audio/audio_core/test/api/audio_capturer_test.cc
index 1fc551c..2a03c26 100644
--- a/src/media/audio/audio_core/test/api/audio_capturer_test.cc
+++ b/src/media/audio/audio_core/test/api/audio_capturer_test.cc
@@ -4,6 +4,7 @@
 
 #include <fuchsia/media/cpp/fidl.h>
 #include <lib/zx/clock.h>
+#include <zircon/device/audio.h>
 
 #include "lib/media/audio/cpp/types.h"
 #include "src/media/audio/lib/clock/clone_mono.h"
@@ -23,23 +24,16 @@
     HermeticAudioTest::SetUp();
 
     audio_core_->CreateAudioCapturer(false, audio_capturer_.NewRequest());
-    audio_capturer_.set_error_handler(ErrorHandler());
+    AddErrorHandler(audio_capturer_, "AudioCapturer");
   }
 
   void TearDown() override {
     gain_control_.Unbind();
-
-    EXPECT_EQ(bound_capturer_expected_, audio_capturer_.is_bound());
     audio_capturer_.Unbind();
 
     HermeticAudioTest::TearDown();
   }
 
-  void SetNegativeExpectations() override {
-    HermeticAudioTest::SetNegativeExpectations();
-    bound_capturer_expected_ = false;
-  }
-
   void SetFormat() {
     audio_capturer_->SetPcmStreamType(
         media::CreateAudioStreamType(fuchsia::media::AudioSampleFormat::SIGNED_16, 1, 16000));
@@ -56,8 +50,6 @@
 
   fuchsia::media::AudioCapturerPtr audio_capturer_;
   fuchsia::media::audio::GainControlPtr gain_control_;
-
-  bool bound_capturer_expected_ = true;
 };
 
 class AudioCapturerClockTest : public AudioCapturerTest {
@@ -68,8 +60,9 @@
   zx::clock GetAndValidateReferenceClock() {
     zx::clock clock;
 
-    audio_capturer_->GetReferenceClock(CompletionCallback(
-        [&clock](zx::clock received_clock) { clock = std::move(received_clock); }));
+    audio_capturer_->GetReferenceClock(
+        AddCallback("GetReferenceClock",
+                    [&clock](zx::clock received_clock) { clock = std::move(received_clock); }));
 
     ExpectCallback();
     EXPECT_TRUE(clock.is_valid());
@@ -112,27 +105,21 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  auto callbacks = 0u;
-  audio_capturer_->CaptureAt(
-      0, 0, 4000, [&callbacks](fuchsia::media::StreamPacket) { EXPECT_EQ(0u, callbacks++); });
-  audio_capturer_->CaptureAt(
-      0, 4000, 4000, [&callbacks](fuchsia::media::StreamPacket) { EXPECT_EQ(1u, callbacks++); });
-  audio_capturer_->CaptureAt(
-      0, 8000, 4000, [&callbacks](fuchsia::media::StreamPacket) { EXPECT_EQ(2u, callbacks++); });
-  audio_capturer_->CaptureAt(
-      0, 12000, 4000, [&callbacks](fuchsia::media::StreamPacket) { EXPECT_EQ(3u, callbacks++); });
+  audio_capturer_->CaptureAt(0, 0, 4000, AddCallback("CaptureAt 0"));
+  audio_capturer_->CaptureAt(0, 4000, 4000, AddCallback("CaptureAt 4000"));
+  audio_capturer_->CaptureAt(0, 8000, 4000, AddCallback("CaptureAt 8000"));
+  audio_capturer_->CaptureAt(0, 12000, 4000, AddCallback("CaptureAt 12000"));
 
   // Packets should complete in strict order, with DiscardAllPackets' completion afterward.
-  audio_capturer_->DiscardAllPackets(
-      CompletionCallback([&callbacks]() { EXPECT_EQ(4u, callbacks); }));
+  audio_capturer_->DiscardAllPackets(AddCallback("DiscardAllPackets"));
   ExpectCallback();
 }
 
 TEST_F(AudioCapturerTest, DiscardAll_WithNoVmoShouldDisconnect) {
   SetFormat();
 
-  audio_capturer_->DiscardAllPackets(CompletionCallback());
-  ExpectDisconnect();
+  audio_capturer_->DiscardAllPackets(AddUnexpectedCallback("DiscardAllPackets"));
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPackets should fail, if async capture is active
@@ -140,13 +127,12 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
-  audio_capturer_->DiscardAllPackets(CompletionCallback());
-  ExpectDisconnect();
+  audio_capturer_->DiscardAllPackets(AddUnexpectedCallback("DiscardAllPackets"));
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPackets should fail, if async capture is in the process of stopping
@@ -154,14 +140,13 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
   audio_capturer_->StopAsyncCaptureNoReply();
-  audio_capturer_->DiscardAllPackets(CompletionCallback());
-  ExpectDisconnect();
+  audio_capturer_->DiscardAllPackets(AddUnexpectedCallback("DiscardAllPackets"));
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPackets should succeed, if async capture is completely stopped
@@ -169,15 +154,14 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
-  audio_capturer_->StopAsyncCapture(CompletionCallback());
+  audio_capturer_->StopAsyncCapture(AddCallback("StopAsyncCapture"));
   ExpectCallback();
 
-  audio_capturer_->DiscardAllPackets(CompletionCallback());
+  audio_capturer_->DiscardAllPackets(AddCallback("DiscardAllPackets"));
   ExpectCallback();
 }
 
@@ -186,7 +170,7 @@
   SetFormat();
 
   audio_capturer_->DiscardAllPacketsNoReply();
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPacketsNoReply should fail, if async capture is active
@@ -194,13 +178,12 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
   audio_capturer_->DiscardAllPacketsNoReply();
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPacketsNoReply should fail, if async capture is in the process of stopping
@@ -208,14 +191,13 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
   audio_capturer_->StopAsyncCaptureNoReply();
   audio_capturer_->DiscardAllPacketsNoReply();
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // DiscardAllPacketsNoReply should succeed, if async capture is completely stopped
@@ -223,12 +205,11 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
-  audio_capturer_->StopAsyncCapture(CompletionCallback());
+  audio_capturer_->StopAsyncCapture(AddCallback("StopAsyncCapture"));
   ExpectCallback();
 
   audio_capturer_->DiscardAllPacketsNoReply();
@@ -252,14 +233,14 @@
 // Also negative testing: 0/tiny/huge num frames (bigger than packet)
 
 TEST_F(AudioCapturerTest, Stop_WhenStoppedShouldDisconnect) {
-  audio_capturer_->StopAsyncCapture(CompletionCallback());
-  ExpectDisconnect();
+  audio_capturer_->StopAsyncCapture(AddUnexpectedCallback("StopAsyncCapture"));
+  ExpectDisconnect(audio_capturer_);
 }
 // Also test before format set, before packets submitted
 
 TEST_F(AudioCapturerTest, StopNoReply_WhenStoppedShouldDisconnect) {
   audio_capturer_->StopAsyncCaptureNoReply();
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 // Also before format set, before packets submitted
 
@@ -268,27 +249,16 @@
 // or GainControl binding a chance to disconnect, if an error occurred.
 TEST_F(AudioCapturerTest, BindGainControl) {
   // Validate AudioCapturers can create GainControl interfaces.
-  bool capturer_error_occurred = false;
-  bool capturer_error_occurred_2 = false;
-  bool gain_error_occurred = false;
-  bool gain_error_occurred_2 = false;
-
-  audio_capturer_.set_error_handler(
-      ErrorHandler([&capturer_error_occurred](zx_status_t) { capturer_error_occurred = true; }));
-
   audio_capturer_->BindGainControl(gain_control_.NewRequest());
-  gain_control_.set_error_handler(
-      ErrorHandler([&gain_error_occurred](zx_status_t) { gain_error_occurred = true; }));
+  AddErrorHandler(gain_control_, "AudioCapturer::GainControl");
 
   fuchsia::media::AudioCapturerPtr audio_capturer_2;
   audio_core_->CreateAudioCapturer(true, audio_capturer_2.NewRequest());
-  audio_capturer_2.set_error_handler(ErrorHandler(
-      [&capturer_error_occurred_2](zx_status_t) { capturer_error_occurred_2 = true; }));
+  AddErrorHandler(audio_capturer_2, "AudioCapturer2");
 
   fuchsia::media::audio::GainControlPtr gain_control_2;
   audio_capturer_2->BindGainControl(gain_control_2.NewRequest());
-  gain_control_2.set_error_handler(
-      ErrorHandler([&gain_error_occurred_2](zx_status_t) { gain_error_occurred_2 = true; }));
+  AddErrorHandler(gain_control_2, "AudioCapturer::GainControl2");
 
   // What happens to a child gain_control, when a capturer is unbound?
   audio_capturer_.Unbind();
@@ -297,27 +267,11 @@
   gain_control_2.Unbind();
 
   // Give audio_capturer_ a chance to disconnect gain_control_
-  ExpectDisconnect();
-
-  // If gain_control_ disconnected as expected, reset errors for the next step.
-  if (gain_error_occurred) {
-    error_expected_ = false;
-    error_occurred_ = false;
-  }
+  ExpectDisconnect(gain_control_);
 
   // Give time for other Disconnects to occur, if they must.
-  audio_capturer_2->GetStreamType(CompletionCallback([](fuchsia::media::StreamType) {}));
+  audio_capturer_2->GetStreamType(AddCallback("GetStreamType"));
   ExpectCallback();
-
-  // Explicitly unbinding audio_capturer_ should disconnect gain_control_.
-  EXPECT_FALSE(capturer_error_occurred);
-  EXPECT_TRUE(gain_error_occurred);
-  EXPECT_FALSE(gain_control_.is_bound());
-
-  // gain_2's parent should NOT disconnect, nor a gain_2 disconnect callback.
-  EXPECT_FALSE(capturer_error_occurred_2);
-  EXPECT_FALSE(gain_error_occurred_2);
-  EXPECT_TRUE(audio_capturer_2.is_bound());
 }
 
 // Null requests to BindGainControl should have no effect.
@@ -325,7 +279,7 @@
   audio_capturer_->BindGainControl(nullptr);
 
   // Give time for Disconnect to occur, if it must.
-  audio_capturer_->GetStreamType(CompletionCallback([](fuchsia::media::StreamType) {}));
+  audio_capturer_->GetStreamType(AddCallback("GetStreamType"));
   ExpectCallback();
 }
 
@@ -415,7 +369,7 @@
 
   audio_capturer_->SetReferenceClock(std::move(dupe_clock));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // inadequate ZX_RIGHTS -- no READ should cause GetReferenceClock to fail.
@@ -425,7 +379,7 @@
 
   audio_capturer_->SetReferenceClock(std::move(dupe_clock));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // If client-submitted clock has ZX_RIGHT_WRITE, this should be removed upon GetReferenceClock
@@ -455,7 +409,7 @@
 
   audio_capturer_->CaptureAt(0, 0, 8000, [](fuchsia::media::StreamPacket) { FAIL(); });
   audio_capturer_->SetReferenceClock(clock::CloneOfMonotonic());
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // Setting the reference clock should succeed, after all active capture packets have returned
@@ -463,7 +417,7 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_->CaptureAt(0, 0, 8000, CompletionCallback([](fuchsia::media::StreamPacket) {}));
+  audio_capturer_->CaptureAt(0, 0, 8000, AddCallback("CaptureAt"));
   ExpectCallback();
 
   audio_capturer_->SetReferenceClock(clock::AdjustableCloneOfMonotonic());
@@ -476,7 +430,7 @@
   SetUpPayloadBuffer();
 
   audio_capturer_->CaptureAt(0, 0, 8000, [](fuchsia::media::StreamPacket) {});
-  audio_capturer_->DiscardAllPackets(CompletionCallback());
+  audio_capturer_->DiscardAllPackets(AddCallback("DiscardAllPackets"));
   ExpectCallback();
 
   audio_capturer_->SetReferenceClock(clock::AdjustableCloneOfMonotonic());
@@ -488,13 +442,12 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
   audio_capturer_->SetReferenceClock(clock::CloneOfMonotonic());
-  ExpectDisconnect();
+  ExpectDisconnect(audio_capturer_);
 }
 
 // Setting the reference clock should succeed, after all active capture packets have returned
@@ -502,12 +455,11 @@
   SetFormat();
   SetUpPayloadBuffer();
 
-  audio_capturer_.events().OnPacketProduced =
-      CompletionCallback([](fuchsia::media::StreamPacket) {});
+  audio_capturer_.events().OnPacketProduced = AddCallback("OnPacketProduced");
   audio_capturer_->StartAsyncCapture(1600);
   ExpectCallback();
 
-  audio_capturer_->StopAsyncCapture(CompletionCallback());
+  audio_capturer_->StopAsyncCapture(AddCallback("StopAsyncCapture"));
   ExpectCallback();
 
   audio_capturer_->SetReferenceClock(clock::AdjustableCloneOfMonotonic());
diff --git a/src/media/audio/audio_core/test/api/audio_renderer_test.cc b/src/media/audio/audio_core/test/api/audio_renderer_test.cc
index da015ce..0953f3a 100644
--- a/src/media/audio/audio_core/test/api/audio_renderer_test.cc
+++ b/src/media/audio/audio_core/test/api/audio_renderer_test.cc
@@ -33,7 +33,6 @@
  protected:
   void SetUp() override;
   void TearDown() override;
-  void SetNegativeExpectations() override;
 
   // Discards all in-flight packets and waits for the response from the audio
   // renderer. This can be used as a simple round-trip through the audio
@@ -51,8 +50,6 @@
 
   fuchsia::media::AudioRendererPtr audio_renderer_;
   fuchsia::media::audio::GainControlPtr gain_control_;
-
-  bool bound_renderer_expected_ = true;
 };
 
 // AudioRendererClockTest - thin wrapper around AudioRendererTest
@@ -68,25 +65,18 @@
   HermeticAudioTest::SetUp();
 
   audio_core_->CreateAudioRenderer(audio_renderer_.NewRequest());
-  audio_renderer_.set_error_handler(ErrorHandler());
+  AddErrorHandler(audio_renderer_, "AudioRenderer");
 }
 
 void AudioRendererTest::TearDown() {
   gain_control_.Unbind();
-
-  EXPECT_EQ(bound_renderer_expected_, audio_renderer_.is_bound());
   audio_renderer_.Unbind();
 
   HermeticAudioTest::TearDown();
 }
 
-void AudioRendererTest::SetNegativeExpectations() {
-  HermeticAudioTest::SetNegativeExpectations();
-  bound_renderer_expected_ = false;
-}
-
 void AudioRendererTest::AssertConnectedAndDiscardAllPackets() {
-  audio_renderer_->DiscardAllPackets(CompletionCallback());
+  audio_renderer_->DiscardAllPackets(AddCallback("DiscardAllPackets"));
 
   ExpectCallback();
 }
@@ -141,7 +131,7 @@
   // should fail.
   CreateAndAddPayloadBuffer(0);
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // Test removing payload buffers.
@@ -161,7 +151,7 @@
 TEST_F(AudioRendererTest, RemovePayloadBuffer_InvalidBufferIdShouldDisconnect) {
   audio_renderer_->RemovePayloadBuffer(0);
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // It is invalid to remove a payload buffer while there are queued packets.
@@ -182,7 +172,7 @@
   // should fail.
   audio_renderer_->RemovePayloadBuffer(0);
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 //
@@ -202,14 +192,11 @@
   packet.payload_buffer_id = 0;
   packet.payload_offset = 0;
   packet.payload_size = kValidPayloadSize;
-  bool callback_received = false;
-  audio_renderer_->SendPacket(std::move(packet),
-                              [&callback_received] { callback_received = true; });
+  audio_renderer_->SendPacket(std::move(packet), AddCallback("SendPacket"));
 
   audio_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
                         [](int64_t, int64_t) {});
-  RunLoopUntil([this, &callback_received]() { return error_occurred_ || callback_received; });
-  EXPECT_TRUE(callback_received);
+  ExpectCallback();
 }
 
 TEST_F(AudioRendererTest, SendPacket_InvokesCallbacksInOrder) {
@@ -222,22 +209,15 @@
   packet.payload_buffer_id = 0;
   packet.payload_offset = 0;
   packet.payload_size = kValidPayloadSize;
-  uint32_t callback_count = 0;
-  audio_renderer_->SendPacket(fidl::Clone(packet),
-                              [&callback_count] { EXPECT_EQ(0u, callback_count++); });
-  audio_renderer_->SendPacket(fidl::Clone(packet),
-                              [&callback_count] { EXPECT_EQ(1u, callback_count++); });
-  audio_renderer_->SendPacket(fidl::Clone(packet),
-                              [&callback_count] { EXPECT_EQ(2u, callback_count++); });
-  audio_renderer_->SendPacket(fidl::Clone(packet),
-                              [&callback_count] { EXPECT_EQ(3u, callback_count++); });
+  audio_renderer_->SendPacket(fidl::Clone(packet), AddCallback("SendPacket1"));
+  audio_renderer_->SendPacket(fidl::Clone(packet), AddCallback("SendPacket2"));
+  audio_renderer_->SendPacket(fidl::Clone(packet), AddCallback("SendPacket3"));
+  audio_renderer_->SendPacket(fidl::Clone(packet), AddCallback("SendPacket4"));
 
   // Play and expect the callbacks in order.
   audio_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
                         [](int64_t, int64_t) {});
-
-  RunLoopUntil([this, &callback_count]() { return error_occurred_ || (callback_count == 4u); });
-  EXPECT_EQ(4u, callback_count);
+  ExpectCallback();
 }
 
 TEST_F(AudioRendererTest, SendPackets_TooManyShouldDisconnect) {
@@ -261,8 +241,7 @@
   for (int i = 0; i < 600; ++i) {
     audio_renderer_->SendPacketNoReply(std::move(packet));
   }
-  SetNegativeExpectations();
-  RunLoopUntil([this]() { return error_occurred_; });
+  ExpectDisconnect(audio_renderer_);
 }
 
 //
@@ -296,7 +275,7 @@
   packet.payload_size = kValidPayloadSize;
   audio_renderer_->SendPacketNoReply(std::move(packet));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // It is invalid to SendPacket before the stream type has been configured
@@ -313,7 +292,7 @@
   packet.payload_size = kValidPayloadSize;
   audio_renderer_->SendPacketNoReply(std::move(packet));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // SendPacket with a |payload_size| that is invalid.
@@ -329,7 +308,7 @@
   packet.payload_size = kInvalidPayloadSize;
   audio_renderer_->SendPacketNoReply(std::move(packet));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 TEST_F(AudioRendererTest, SendPacketNoReply_BufferOutOfBoundsShouldDisconnect) {
@@ -345,7 +324,7 @@
   packet.payload_size = kValidPayloadSize;
   audio_renderer_->SendPacketNoReply(std::move(packet));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 TEST_F(AudioRendererTest, SendPacketNoReply_BufferOverrunShouldDisconnect) {
@@ -361,7 +340,7 @@
   packet.payload_offset = kDefaultPayloadBufferSize - kValidPayloadSize;
   audio_renderer_->SendPacketNoReply(std::move(packet));
 
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // TODO(mpuryear): test EndOfStream();
@@ -378,7 +357,7 @@
 
   audio_renderer_->Play(
       fuchsia::media::NO_TIMESTAMP, 0,
-      CompletionCallback([&play_ref_time, &play_media_time](auto ref_time, auto media_time) {
+      AddCallback("Play", [&play_ref_time, &play_media_time](auto ref_time, auto media_time) {
         play_ref_time = ref_time;
         play_media_time = media_time;
       }));
@@ -395,7 +374,7 @@
   zx_nanosleep(play_ref_time);
 
   audio_renderer_->Pause(
-      CompletionCallback([&pause_ref_time, &pause_media_time](auto ref_time, auto media_time) {
+      AddCallback("Pause", [&pause_ref_time, &pause_media_time](auto ref_time, auto media_time) {
         pause_ref_time = ref_time;
         pause_media_time = media_time;
       }));
@@ -426,15 +405,13 @@
   packet1.payload_offset = packet2.payload_offset = packet3.payload_offset = 0;
   packet1.payload_size = packet2.payload_size = packet3.payload_size = kDefaultPayloadBufferSize;
 
-  auto callbacks = 0u;
-  audio_renderer_->SendPacket(std::move(packet1), [&callbacks]() { EXPECT_EQ(0u, callbacks++); });
-  audio_renderer_->SendPacket(std::move(packet2), [&callbacks]() { EXPECT_EQ(1u, callbacks++); });
-  audio_renderer_->SendPacket(std::move(packet3), [&callbacks]() { EXPECT_EQ(2u, callbacks++); });
+  audio_renderer_->SendPacket(std::move(packet1), AddCallback("SendPacket1"));
+  audio_renderer_->SendPacket(std::move(packet2), AddCallback("SendPacket2"));
+  audio_renderer_->SendPacket(std::move(packet3), AddCallback("SendPacket3"));
   audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
 
   // Packets must complete in order, with the DiscardAllPackets completion afterward.
-  audio_renderer_->DiscardAllPackets(
-      CompletionCallback([&callbacks]() { EXPECT_EQ(3u, callbacks); }));
+  audio_renderer_->DiscardAllPackets(AddCallback("DiscardAllPackets"));
   ExpectCallback();
 }
 
@@ -488,7 +465,7 @@
   audio_renderer_->SetPcmStreamType(format2);
 
   // Allow an error Disconnect callback, but we expect a timeout instead.
-  audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
+  audio_renderer_->GetMinLeadTime(AddCallback("GetMinLeadTime"));
   ExpectCallback();
 }
 
@@ -508,7 +485,7 @@
   packet.payload_buffer_id = 0;
   packet.payload_offset = 0;
   packet.payload_size = kValidPayloadSize;
-  audio_renderer_->SendPacket(std::move(packet), CompletionCallback());
+  audio_renderer_->SendPacket(std::move(packet), AddCallback("SendPacket"));
 
   int64_t ref_time_received = -1;
   int64_t media_time_received = -1;
@@ -536,7 +513,7 @@
   packet.payload_buffer_id = 0;
   packet.payload_offset = 0;
   packet.payload_size = kValidPayloadSize;
-  audio_renderer_->SendPacket(std::move(packet), CompletionCallback());
+  audio_renderer_->SendPacket(std::move(packet), AddCallback("SendPacket"));
 
   audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
   ExpectCallback();
@@ -553,15 +530,15 @@
 // Validate MinLeadTime events, when enabled.
 TEST_F(AudioRendererTest, EnableMinLeadTimeEvents) {
   int64_t min_lead_time = -1;
-  audio_renderer_.events().OnMinLeadTimeChanged = [&min_lead_time](int64_t min_lead_time_nsec) {
-    min_lead_time = min_lead_time_nsec;
-  };
+  audio_renderer_.events().OnMinLeadTimeChanged = AddCallback(
+      "OnMinLeadTimeChanged",
+      [&min_lead_time](int64_t min_lead_time_nsec) { min_lead_time = min_lead_time_nsec; });
 
   audio_renderer_->EnableMinLeadTimeEvents(true);
 
   // After enabling MinLeadTime events, we expect an initial notification.
   // Because we have not yet set the format, we expect MinLeadTime to be 0.
-  RunLoopUntil([this, &min_lead_time]() { return error_occurred_ || (min_lead_time >= 0); });
+  ExpectCallback();
   EXPECT_EQ(min_lead_time, 0);
 
   // FYI: after setting format, MinLeadTime should change to be greater than 0
@@ -571,14 +548,15 @@
 
 // Validate MinLeadTime events, when disabled.
 TEST_F(AudioRendererTest, DisableMinLeadTimeEvents) {
-  audio_renderer_.events().OnMinLeadTimeChanged =
-      CompletionCallback([](int64_t x) { EXPECT_FALSE(true) << kCallbackErr; });
+  audio_renderer_.events().OnMinLeadTimeChanged = [](int64_t x) {
+    ADD_FAILURE() << "Unexpected call to OnMinLeadTimeChanged";
+  };
 
   audio_renderer_->EnableMinLeadTimeEvents(false);
 
   // We should not receive a OnMinLeadTimeChanged callback (or Disconnect)
   // before receiving this direct GetMinLeadTime callback.
-  audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
+  audio_renderer_->GetMinLeadTime(AddCallback("GetMinLeadTime"));
   ExpectCallback();
 }
 
@@ -587,11 +565,12 @@
 // Before SetPcmStreamType is called, MinLeadTime should equal zero.
 TEST_F(AudioRendererTest, GetMinLeadTime) {
   int64_t min_lead_time = -1;
-  audio_renderer_->GetMinLeadTime(
-      [&min_lead_time](int64_t min_lead_time_nsec) { min_lead_time = min_lead_time_nsec; });
+  audio_renderer_->GetMinLeadTime(AddCallback(
+      "GetMinLeadTime",
+      [&min_lead_time](int64_t min_lead_time_nsec) { min_lead_time = min_lead_time_nsec; }));
 
   // Wait to receive Lead time callback (will loop timeout? EXPECT_FALSE)
-  RunLoopUntil([this, &min_lead_time]() { return error_occurred_ || (min_lead_time >= 0); });
+  ExpectCallback();
   EXPECT_EQ(min_lead_time, 0);
 }
 
@@ -601,21 +580,15 @@
 TEST_F(AudioRendererTest, BindGainControl) {
   // Validate AudioRenderers can create GainControl interfaces.
   audio_renderer_->BindGainControl(gain_control_.NewRequest());
-  bool gc_error_occurred = false;
-  auto gc_err_handler = [&gc_error_occurred](zx_status_t error) { gc_error_occurred = true; };
-  gain_control_.set_error_handler(gc_err_handler);
+  AddErrorHandler(gain_control_, "AudioRenderer::GainControl");
 
   fuchsia::media::AudioRendererPtr audio_renderer_2;
   audio_core_->CreateAudioRenderer(audio_renderer_2.NewRequest());
-  bool ar2_error_occurred = false;
-  auto ar2_err_handler = [&ar2_error_occurred](zx_status_t error) { ar2_error_occurred = true; };
-  audio_renderer_2.set_error_handler(ar2_err_handler);
+  AddErrorHandler(audio_renderer_2, "AudioRenderer2");
 
   fuchsia::media::audio::GainControlPtr gain_control_2;
   audio_renderer_2->BindGainControl(gain_control_2.NewRequest());
-  bool gc2_error_occurred = false;
-  auto gc2_err_handler = [&gc2_error_occurred](zx_status_t error) { gc2_error_occurred = true; };
-  gain_control_2.set_error_handler(gc2_err_handler);
+  AddErrorHandler(gain_control_2, "AudioRenderer::GainControl2");
 
   // Validate GainControl2 does NOT persist after audio_renderer_2 is unbound
   audio_renderer_2.Unbind();
@@ -624,24 +597,11 @@
   gain_control_.Unbind();
 
   // Give audio_renderer_2 a chance to disconnect gain_control_2
-  RunLoopUntil([this, &ar2_error_occurred, &gc_error_occurred, &gc2_error_occurred]() {
-    return (error_occurred_ || ar2_error_occurred || gc_error_occurred || gc2_error_occurred);
-  });
+  ExpectDisconnect(gain_control_2);
 
   // Let audio_renderer_ show it is still alive (and allow other disconnects)
-  audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
+  audio_renderer_->GetMinLeadTime(AddCallback("GetMinLeadTime"));
   ExpectCallback();
-
-  // Explicitly unbinding audio_renderer_2 should not trigger its disconnect
-  // (ar2_error_occurred), but should trigger gain_control_2's disconnect.
-  EXPECT_FALSE(ar2_error_occurred);
-  EXPECT_TRUE(gc2_error_occurred);
-  EXPECT_FALSE(gain_control_2.is_bound());
-
-  // Explicitly unbinding gain_control_ should not trigger its disconnect, nor
-  // its parent audio_renderer_'s.
-  EXPECT_FALSE(gc_error_occurred);
-  EXPECT_TRUE(audio_renderer_.is_bound());
 }
 
 // Before setting format, Play should not succeed.
@@ -657,7 +617,7 @@
       });
 
   // Disconnect callback should be received
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
   EXPECT_EQ(ref_time_received, -1);
   EXPECT_EQ(media_time_received, -1);
 }
@@ -681,7 +641,7 @@
       });
 
   // Disconnect callback should be received
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
   EXPECT_EQ(ref_time_received, -1);
   EXPECT_EQ(media_time_received, -1);
 }
@@ -691,7 +651,7 @@
   audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
 
   // Disconnect callback should be received.
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // Before setting format, Pause should not succeed.
@@ -706,7 +666,7 @@
       });
 
   // Disconnect callback should be received
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
   EXPECT_EQ(ref_time_received, -1);
   EXPECT_EQ(media_time_received, -1);
 }
@@ -729,7 +689,7 @@
       });
 
   // Disconnect callback should be received
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
   EXPECT_EQ(ref_time_received, -1);
   EXPECT_EQ(media_time_received, -1);
 }
@@ -739,7 +699,7 @@
   audio_renderer_->PauseNoReply();
 
   // Disconnect callback should be received.
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 TEST_F(AudioRendererTest, SetUsage_AfterSetPcmStreamTypeShouldDisconnect) {
@@ -748,18 +708,18 @@
   AssertConnectedAndDiscardAllPackets();
 
   audio_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 zx::clock AudioRendererClockTest::GetAndValidateReferenceClock() {
   zx::clock clock;
 
-  audio_renderer_->GetReferenceClock(CompletionCallback(
-      [&clock](zx::clock received_clock) { clock = std::move(received_clock); }));
+  audio_renderer_->GetReferenceClock(
+      AddCallback("GetReferenceClock",
+                  [&clock](zx::clock received_clock) { clock = std::move(received_clock); }));
 
   ExpectCallback();
   EXPECT_TRUE(clock.is_valid());
-  EXPECT_FALSE(error_occurred());
 
   return clock;
 }
@@ -849,7 +809,7 @@
   ASSERT_EQ(orig_clock.duplicate(kClockRights & ~ZX_RIGHT_DUPLICATE, &dupe_clock), ZX_OK);
 
   audio_renderer_->SetReferenceClock(std::move(dupe_clock));
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // inadequate ZX_RIGHTS -- no READ should cause GetReferenceClock to fail.
@@ -858,7 +818,7 @@
   ASSERT_EQ(orig_clock.duplicate(kClockRights & ~ZX_RIGHT_READ, &dupe_clock), ZX_OK);
 
   audio_renderer_->SetReferenceClock(std::move(dupe_clock));
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // If client-submitted clock has ZX_RIGHT_WRITE, this should be removed upon GetReferenceClock
@@ -881,7 +841,7 @@
   GetAndValidateReferenceClock();
 
   audio_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
-                        CompletionCallback([](int64_t, int64_t) {}));
+                        AddCallback("Play"));
   ExpectCallback();
 
   // We are now playing, but there are no active packets.
@@ -904,7 +864,7 @@
   audio_renderer_->SendPacketNoReply(std::move(packet2));
 
   audio_renderer_->SetReferenceClock(clock::CloneOfMonotonic());
-  ExpectDisconnect();
+  ExpectDisconnect(audio_renderer_);
 }
 
 // Setting the reference clock should succeed, after all active render packets have returned
@@ -917,7 +877,7 @@
   packet.payload_buffer_id = 0;
   packet.payload_offset = 0;
   packet.payload_size = kValidPayloadSize;
-  audio_renderer_->SendPacket(std::move(packet), CompletionCallback());
+  audio_renderer_->SendPacket(std::move(packet), AddCallback("SendPacket"));
   audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
 
   // Wait for the packet completion; now there are no active packets.
diff --git a/src/media/audio/audio_core/test/api/audio_tuner_test.cc b/src/media/audio/audio_core/test/api/audio_tuner_test.cc
index 612e4ff..5567896 100644
--- a/src/media/audio/audio_core/test/api/audio_tuner_test.cc
+++ b/src/media/audio/audio_core/test/api/audio_tuner_test.cc
@@ -30,9 +30,8 @@
 TEST_F(AudioTunerTest, ConnectToAudioTuner) {
   fuchsia::media::tuning::AudioTunerPtr audio_tuner;
   environment()->ConnectToService(audio_tuner.NewRequest());
-  audio_tuner.set_error_handler(ErrorHandler());
-  audio_tuner->GetAvailableAudioEffects(
-      CompletionCallback([](std::vector<fuchsia::media::tuning::AudioEffectType>) {}));
+  AddErrorHandler(audio_tuner, "AudioTuner");
+  audio_tuner->GetAvailableAudioEffects(AddCallback("GetAvailableAudioEffects"));
   ExpectCallback();
 }
 
diff --git a/src/media/audio/audio_core/test/api/gain_control_test.cc b/src/media/audio/audio_core/test/api/gain_control_test.cc
index be696e1..b763fd6 100644
--- a/src/media/audio/audio_core/test/api/gain_control_test.cc
+++ b/src/media/audio/audio_core/test/api/gain_control_test.cc
@@ -2,541 +2,232 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "src/media/audio/audio_core/test/api/gain_control_test.h"
+#include <fuchsia/media/cpp/fidl.h>
 
 #include <cmath>
 
 #include <gtest/gtest.h>
 
+#include "src/media/audio/lib/test/hermetic_audio_test.h"
+
+// TYPED_TEST_SUITE uses RTTI to print type names, but RTTI is disabled in our build, so
+// specialize this function to get nicer test failure messages.
+namespace testing::internal {
+template <>
+std::string GetTypeName<fuchsia::media::AudioRendererPtr>() {
+  return "AudioRenderer";
+}
+template <>
+std::string GetTypeName<fuchsia::media::AudioCapturerPtr>() {
+  return "AudioCapturer";
+}
+}  // namespace testing::internal
+
 namespace media::audio::test {
 
-// GainControlTestBase
-//
-void GainControlTestBase::TearDown() {
-  EXPECT_EQ(!gain_control_.is_bound(), null_gain_control_expected_);
-  gain_control_.Unbind();
+namespace {
+template <typename RendererOrCapturerT>
+struct RendererOrCapturerTraits {};
 
-  EXPECT_EQ(error_occurred_2_, error_expected_2_);
-  EXPECT_EQ(!gain_control_2_.is_bound(), null_gain_control_expected_2_);
-  gain_control_2_.Unbind();
+template <>
+struct RendererOrCapturerTraits<fuchsia::media::AudioRendererPtr> {
+  static std::string Name() { return "AudioRenderer"; }
+  static void Create(fuchsia::media::AudioCorePtr& audio_core,
+                     fuchsia::media::AudioRendererPtr& p) {
+    audio_core->CreateAudioRenderer(p.NewRequest());
+  }
+};
 
-  // These expect_ vars indicate negative cases where we expect failure.
-  EXPECT_EQ(ApiIsNull(), null_api_expected_);
-  audio_renderer_.Unbind();
-  audio_capturer_.Unbind();
-  audio_renderer_2_.Unbind();
-  audio_capturer_2_.Unbind();
+template <>
+struct RendererOrCapturerTraits<fuchsia::media::AudioCapturerPtr> {
+  static std::string Name() { return "AudioCapturer"; }
+  static void Create(fuchsia::media::AudioCorePtr& audio_core,
+                     fuchsia::media::AudioCapturerPtr& p) {
+    audio_core->CreateAudioCapturer(false, p.NewRequest());
+  }
+};
+}  // namespace
 
-  HermeticAudioTest::TearDown();
-}
+template <typename RendererOrCapturerT>
+class GainControlTest : public HermeticAudioTest {
+ protected:
+  void SetUp() override {
+    HermeticAudioTest::SetUp();
 
-void GainControlTestBase::SetUpRenderer() {
-  audio_core_->CreateAudioRenderer(audio_renderer_.NewRequest());
-  audio_renderer_.set_error_handler(ErrorHandler());
-}
+    // Create two gain controllers tied to the same parent object. We will manipulate
+    // gain_control_1_ while expecting events on both gain controllers.
+    RendererOrCapturerTraits<RendererOrCapturerT>::Create(audio_core_, parent_);
+    AddErrorHandler(parent_, RendererOrCapturerTraits<RendererOrCapturerT>::Name());
 
-void GainControlTestBase::SetUpCapturer() {
-  audio_core_->CreateAudioCapturer(false, audio_capturer_.NewRequest());
-  audio_capturer_.set_error_handler(ErrorHandler());
-}
+    // Bind gc2 first. If we do this in the opposite order, then commands sent to gc1
+    // might happen concurrently with the binding of gc2, meaning gc2 will miss updates.
+    parent_->BindGainControl(gain_control_2_.NewRequest());
+    parent_->BindGainControl(gain_control_1_.NewRequest());
+    AddErrorHandler(gain_control_1_,
+                    RendererOrCapturerTraits<RendererOrCapturerT>::Name() + "::GainControl1");
+    AddErrorHandler(gain_control_2_,
+                    RendererOrCapturerTraits<RendererOrCapturerT>::Name() + "::GainControl2");
 
-void GainControlTestBase::SetUpRenderer2() {
-  audio_core_->CreateAudioRenderer(audio_renderer_2_.NewRequest());
-  audio_renderer_2_.set_error_handler(
-      ErrorHandler([this](zx_status_t) { error_occurred_2_ = true; }));
-}
+    // To ensure there is no crosstalk, we create a dummy renderer and capturer
+    // and a gain control for each, and verify those gain controls are not called.
+    audio_core_->CreateAudioRenderer(unused_renderer_.NewRequest());
+    audio_core_->CreateAudioCapturer(false, unused_capturer_.NewRequest());
+    AddErrorHandler(unused_renderer_, "AudioRenderer (unused)");
+    AddErrorHandler(unused_capturer_, "AudioCapturer (unused)");
 
-void GainControlTestBase::SetUpCapturer2() {
-  audio_core_->CreateAudioCapturer(false, audio_capturer_2_.NewRequest());
-  audio_capturer_2_.set_error_handler(
-      ErrorHandler([this](zx_status_t) { error_occurred_2_ = true; }));
-}
-
-void GainControlTestBase::SetUpGainControl() {
-  gain_control_.set_error_handler(ErrorHandler());
-
-  gain_control_.events().OnGainMuteChanged = CompletionCallback([this](float gain_db, bool muted) {
-    received_gain_db_ = gain_db;
-    received_mute_ = muted;
-  });
-
-  null_gain_control_expected_ = false;
-}
-
-void GainControlTestBase::SetUpGainControlOnRenderer() {
-  audio_renderer_->BindGainControl(gain_control_.NewRequest());
-  SetUpGainControl();
-}
-
-void GainControlTestBase::SetUpGainControlOnCapturer() {
-  audio_capturer_->BindGainControl(gain_control_.NewRequest());
-  SetUpGainControl();
-}
-
-void GainControlTestBase::SetUpGainControl2() {
-  gain_control_2_.set_error_handler(
-      ErrorHandler([this](zx_status_t) { error_occurred_2_ = true; }));
-
-  gain_control_2_.events().OnGainMuteChanged =
-      CompletionCallback([this](float gain_db, bool muted) {
-        received_gain_db_2_ = gain_db;
-        received_mute_2_ = muted;
-      });
-
-  null_gain_control_expected_2_ = false;
-}
-
-void GainControlTestBase::SetUpGainControl2OnRenderer() {
-  audio_renderer_->BindGainControl(gain_control_2_.NewRequest());
-  SetUpGainControl2();
-}
-
-void GainControlTestBase::SetUpGainControl2OnCapturer() {
-  audio_capturer_->BindGainControl(gain_control_2_.NewRequest());
-  SetUpGainControl2();
-}
-
-void GainControlTestBase::SetUpGainControl2OnRenderer2() {
-  audio_renderer_2_->BindGainControl(gain_control_2_.NewRequest());
-  SetUpGainControl2();
-}
-
-void GainControlTestBase::SetUpGainControl2OnCapturer2() {
-  audio_capturer_2_->BindGainControl(gain_control_2_.NewRequest());
-  SetUpGainControl2();
-}
-
-// For tests that cause a GainControl to disconnect, set these expectations.
-void GainControlTestBase::SetNegativeExpectations() {
-  HermeticAudioTest::SetNegativeExpectations();
-
-  null_api_expected_ = true;
-  null_gain_control_expected_ = true;
-}
-
-// Set Gain, asserting that state is already reset so error can be detected.
-void GainControlTestBase::SetGain(float gain_db) { gain_control_->SetGain(gain_db); }
-
-// Set Mute, asserting that state is already reset so error can be detected.
-void GainControlTestBase::SetMute(bool mute) { gain_control_->SetMute(mute); }
-
-// Expect and absorb a single gain callback; perform related error checking.
-void GainControlTestBase::ExpectGainCallback(float gain_db, bool mute) {
-  received_gain_db_ = kTooLowGainDb;
-
-  RunLoopUntil([this, &gain_db, &mute]() {
-    return (received_gain_db_ == gain_db) && (received_mute_ == mute);
-  });
-  EXPECT_EQ(received_gain_db_, gain_db);
-  EXPECT_EQ(received_mute_, mute);
-  EXPECT_FALSE(error_occurred_);
-}
-
-// Tests expect to receive a disconnect callback for API binding, then for
-// GainControl binding. Treat any regular gain callback received as error.
-void GainControlTestBase::ExpectDisconnect() {
-  // Need to wait for both renderer/capturer AND gain_control to disconnect.
-  HermeticAudioTest::ExpectDisconnect();
-
-  if (gain_control_.is_bound() || !ApiIsNull()) {
-    // Reset our error detector before listening again.
-    error_occurred_ = false;
-    HermeticAudioTest::ExpectDisconnect();
+    SetUpUnusedGainControl(unused_renderer_gain_control_, unused_renderer_);
+    SetUpUnusedGainControl(unused_capturer_gain_control_, unused_capturer_);
   }
 
-  EXPECT_TRUE(ApiIsNull());
-  EXPECT_FALSE(gain_control_.is_bound());
-}
+  template <typename ParentT>
+  void SetUpUnusedGainControl(fuchsia::media::audio::GainControlPtr& gc, ParentT& parent) {
+    parent->BindGainControl(gc.NewRequest());
+    AddErrorHandler(gc, RendererOrCapturerTraits<ParentT>::Name() + "::GainControl (unused)");
 
-// Test implementations, called by various objects across the class hierarchy
-void GainControlTestBase::TestSetGain() {
+    gc.events().OnGainMuteChanged = [](float gain_db, bool muted) {
+      ADD_FAILURE() << "Unexpected call to unused " << RendererOrCapturerTraits<ParentT>::Name()
+                    << "'s GainControl: OnGainMuteChanged(" << gain_db << ", " << muted << ")";
+    };
+  }
+
+  void ExpectGainCallback(float expected_gain_db, bool expected_mute) {
+    float received_gain_db_1 = NAN;
+    float received_gain_db_2 = NAN;
+    bool received_mute_1 = false;
+    bool received_mute_2 = false;
+
+    // We bound gc2 first, so it gets the event first.
+    gain_control_2_.events().OnGainMuteChanged =
+        AddCallback("GainControl2::OnGainMuteChanged",
+                    [&received_gain_db_2, &received_mute_2](float gain_db, bool muted) {
+                      received_gain_db_2 = gain_db;
+                      received_mute_2 = muted;
+                    });
+    gain_control_1_.events().OnGainMuteChanged =
+        AddCallback("GainControl1::OnGainMuteChanged",
+                    [&received_gain_db_1, &received_mute_1](float gain_db, bool muted) {
+                      received_gain_db_1 = gain_db;
+                      received_mute_1 = muted;
+                    });
+
+    ExpectCallback();
+    EXPECT_EQ(received_gain_db_1, expected_gain_db);
+    EXPECT_EQ(received_gain_db_2, expected_gain_db);
+    EXPECT_EQ(received_mute_1, expected_mute);
+    EXPECT_EQ(received_mute_2, expected_mute);
+  }
+
+  void ExpectParentDisconnect() {
+    // Disconnecting the parent should also disconnnect the GainControls.
+    ExpectDisconnects({ErrorHandlerFor(parent_), ErrorHandlerFor(gain_control_1_),
+                       ErrorHandlerFor(gain_control_2_)});
+  }
+
+  void SetGain(float gain_db) { gain_control_1_->SetGain(gain_db); }
+  void SetMute(bool mute) { gain_control_1_->SetMute(mute); }
+
+  RendererOrCapturerT parent_;
+  fuchsia::media::audio::GainControlPtr gain_control_1_;
+  fuchsia::media::audio::GainControlPtr gain_control_2_;
+
+  fuchsia::media::AudioRendererPtr unused_renderer_;
+  fuchsia::media::AudioCapturerPtr unused_capturer_;
+  fuchsia::media::audio::GainControlPtr unused_renderer_gain_control_;
+  fuchsia::media::audio::GainControlPtr unused_capturer_gain_control_;
+};
+
+using GainControlTestTypes =
+    ::testing::Types<fuchsia::media::AudioRendererPtr, fuchsia::media::AudioCapturerPtr>;
+TYPED_TEST_SUITE(GainControlTest, GainControlTestTypes);
+
+TYPED_TEST(GainControlTest, SetGain) {
   constexpr float expect_gain_db = 20.0f;
 
-  SetGain(expect_gain_db);
-  ExpectGainCallback(expect_gain_db, false);
+  this->SetGain(expect_gain_db);
+  this->ExpectGainCallback(expect_gain_db, false);
 
-  SetGain(kUnityGainDb);
-  ExpectGainCallback(kUnityGainDb, false);
+  this->SetGain(kUnityGainDb);
+  this->ExpectGainCallback(kUnityGainDb, false);
 }
 
-void GainControlTestBase::TestSetMute() {
+TYPED_TEST(GainControlTest, SetMute) {
   bool expect_mute = true;
 
-  SetMute(expect_mute);
-  ExpectGainCallback(kUnityGainDb, expect_mute);
+  this->SetMute(expect_mute);
+  this->ExpectGainCallback(kUnityGainDb, expect_mute);
 
   expect_mute = false;
-  SetMute(expect_mute);
-  ExpectGainCallback(kUnityGainDb, expect_mute);
+  this->SetMute(expect_mute);
+  this->ExpectGainCallback(kUnityGainDb, expect_mute);
 }
 
-void GainControlTestBase::TestSetGainMute() {
+TYPED_TEST(GainControlTest, SetGainMute) {
   constexpr float expect_gain_db = -5.5f;
 
-  SetGain(expect_gain_db);
-  SetMute(true);
+  this->SetGain(expect_gain_db);
+  this->SetMute(true);
 
-  ExpectGainCallback(expect_gain_db, true);
+  this->ExpectGainCallback(expect_gain_db, true);
 }
 
-void GainControlTestBase::TestDuplicateSetGain() {
+TYPED_TEST(GainControlTest, DuplicateSetGain) {
   constexpr float expect_gain_db = 20.0f;
 
-  SetGain(expect_gain_db);
-  ExpectGainCallback(expect_gain_db, false);
+  this->SetGain(expect_gain_db);
+  this->ExpectGainCallback(expect_gain_db, false);
 
-  SetGain(expect_gain_db);
-  SetMute(true);
+  this->SetGain(expect_gain_db);
+  this->SetMute(true);
   // Rather than waiting for "no gain callback", we set an (independent) mute
   // value and expect only a single callback that includes the more recent mute.
-  ExpectGainCallback(expect_gain_db, true);
+  this->ExpectGainCallback(expect_gain_db, true);
 }
 
-void GainControlTestBase::TestDuplicateSetMute() {
+TYPED_TEST(GainControlTest, DuplicateSetMute) {
   constexpr float expect_gain_db = -42.0f;
 
-  SetMute(true);
-  ExpectGainCallback(kUnityGainDb, true);
+  this->SetMute(true);
+  this->ExpectGainCallback(kUnityGainDb, true);
 
-  SetMute(true);
-  SetGain(expect_gain_db);
+  this->SetMute(true);
+  this->SetGain(expect_gain_db);
   // Rather than waiting for "no mute callback", we set an (independent) gain
   // value and expect only a single callback that includes the more recent gain.
-  ExpectGainCallback(expect_gain_db, true);
+  this->ExpectGainCallback(expect_gain_db, true);
 }
 
-// For negative expectations.
-//
 // Setting gain too high should cause a disconnect.
-void GainControlTestBase::TestSetGainTooHigh() {
-  SetNegativeExpectations();
+TYPED_TEST(GainControlTest, SetGainTooHigh) {
+  this->SetGain(kTooHighGainDb);
 
-  constexpr float expect_gain_db = kTooHighGainDb;
-  SetGain(expect_gain_db);
-
-  ExpectDisconnect();
-  EXPECT_FALSE(gain_control_.is_bound());
+  this->ExpectParentDisconnect();
+  EXPECT_FALSE(this->gain_control_1_.is_bound());
+  EXPECT_FALSE(this->gain_control_2_.is_bound());
 }
 
 // Setting gain too low should cause a disconnect.
-void GainControlTestBase::TestSetGainTooLow() {
-  SetNegativeExpectations();
+TYPED_TEST(GainControlTest, SetGainTooLow) {
+  this->SetGain(kTooLowGainDb);
 
-  constexpr float expect_gain_db = kTooLowGainDb;
-  SetGain(expect_gain_db);
-
-  ExpectDisconnect();
-  EXPECT_FALSE(gain_control_.is_bound());
+  this->ExpectParentDisconnect();
+  EXPECT_FALSE(this->gain_control_1_.is_bound());
+  EXPECT_FALSE(this->gain_control_2_.is_bound());
 }
 
-// Setting stream-specific gain to NAN should cause both FIDL channels
-// (renderer/capturer and gain_control) to disconnect.
-void GainControlTestBase::TestSetGainNaN() {
-  SetNegativeExpectations();
+// Setting gain to NAN should cause a disconnect.
+TYPED_TEST(GainControlTest, SetGainNaN) {
+  this->SetGain(NAN);
 
-  constexpr float expect_gain_db = NAN;
-  SetGain(expect_gain_db);
-
-  ExpectDisconnect();
-  EXPECT_FALSE(gain_control_.is_bound());
+  this->ExpectParentDisconnect();
+  EXPECT_FALSE(this->gain_control_1_.is_bound());
+  EXPECT_FALSE(this->gain_control_2_.is_bound());
 }
 
-//
-// Basic GainControl validation with single instance.
-//
-
-// RenderGainControlTest
-//
-void RenderGainControlTest::SetUp() {
-  GainControlTestBase::SetUp();
-
-  SetUpRenderer();
-  SetUpGainControlOnRenderer();
-}
-
-// Single renderer with one gain control: Gain, Mute and GainMute combo.
-//
-TEST_F(RenderGainControlTest, SetGain) { TestSetGain(); }
-
-TEST_F(RenderGainControlTest, SetMute) { TestSetMute(); }
-
-TEST_F(RenderGainControlTest, SetGainMute) { TestSetGainMute(); }
-
 // TODO(mpuryear): Ramp-related tests (render). Relevant FIDL signature is:
 //   SetGainWithRamp(float32 gain_db, int64 duration_ns, RampType ramp_type);
 
 // TODO(mpuryear): Validate GainChange notifications of gainramps.
 
-TEST_F(RenderGainControlTest, DuplicateSetGain) { TestDuplicateSetGain(); }
-
-TEST_F(RenderGainControlTest, DuplicateSetMute) { TestDuplicateSetMute(); }
-
-TEST_F(RenderGainControlTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-TEST_F(RenderGainControlTest, SetGainTooLow) { TestSetGainTooLow(); }
-
-TEST_F(RenderGainControlTest, SetGainNaN) { TestSetGainNaN(); }
-
 // TODO(mpuryear): Ramp-related negative tests, across all scenarios
 
-// CaptureGainControlTest
-//
-void CaptureGainControlTest::SetUp() {
-  GainControlTestBase::SetUp();
-
-  SetUpCapturer();
-  SetUpGainControlOnCapturer();
-}
-
-// Single capturer with one gain control
-//
-TEST_F(CaptureGainControlTest, SetGain) { TestSetGain(); }
-
-TEST_F(CaptureGainControlTest, SetMute) { TestSetMute(); }
-
-TEST_F(CaptureGainControlTest, SetGainMute) { TestSetGainMute(); }
-
-// TODO(mpuryear): Ramp-related tests (capture)
-
-TEST_F(CaptureGainControlTest, DuplicateSetGain) { TestDuplicateSetGain(); }
-// N.B. DuplicateSetMute behavior is tested in CapturerTwoGainControlsTest.
-
-TEST_F(CaptureGainControlTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-TEST_F(CaptureGainControlTest, SetGainTooLow) { TestSetGainTooLow(); }
-
-TEST_F(CaptureGainControlTest, SetGainNaN) { TestSetGainNaN(); }
-
-// SiblingGainControlsTest
-// On a renderer/capturer, sibling GainControls receive identical notifications.
-//
-// For tests that cause a GainControl to disconnect, set these expectations.
-void SiblingGainControlsTest::SetNegativeExpectations() {
-  GainControlTestBase::SetNegativeExpectations();
-
-  null_gain_control_expected_2_ = true;
-  error_expected_2_ = true;
-}
-
-// Tests expect a gain callback on both gain_controls, with the provided gain_db
-// and mute values -- and no errors.
-void SiblingGainControlsTest::ExpectGainCallback(float gain_db, bool mute) {
-  received_gain_db_ = kTooLowGainDb;
-  received_gain_db_2_ = kTooLowGainDb;
-
-  RunLoopUntil([this, gain_db, mute]() {
-    return error_occurred_ || (received_gain_db_ == gain_db && received_gain_db_2_ == gain_db &&
-                               received_mute_ == mute && received_mute_2_ == mute);
-  });
-
-  EXPECT_FALSE(error_occurred_) << kDisconnectErr;
-  EXPECT_FALSE(ApiIsNull());
-  EXPECT_TRUE(gain_control_.is_bound());
-  EXPECT_TRUE(gain_control_2_.is_bound());
-
-  EXPECT_EQ(received_gain_db_, gain_db);
-  EXPECT_EQ(received_gain_db_2_, gain_db);
-  EXPECT_EQ(received_mute_, mute);
-  EXPECT_EQ(received_mute_2_, mute);
-}
-
-// Tests expect to receive a disconnect callback for the API binding, then
-// one for each of the two GainControl bindings. In our loop, we wait until all
-// three of these have occurred. Also, if any normal gain callback is received
-// during this time, it is unexpected and treated as an error.
-void SiblingGainControlsTest::ExpectDisconnect() {
-  SetNegativeExpectations();
-  received_gain_db_2_ = kTooLowGainDb;
-
-  // Wait Renderer/Capturer and BOTH GainControls to disconnect. Because
-  // multiple disconnect callbacks could arrive between our polling interval, we
-  // wait a maximum of three times, checking between them for completion.
-  HermeticAudioTest::ExpectDisconnect();
-  if (!ApiIsNull() || gain_control_.is_bound() || gain_control_2_.is_bound()) {
-    // Reset our error detector before listening again.
-    error_occurred_ = false;
-    HermeticAudioTest::ExpectDisconnect();
-  }
-  if (!ApiIsNull() || gain_control_.is_bound() || gain_control_2_.is_bound()) {
-    // Reset our error detector before listening again.
-    error_occurred_ = false;
-    HermeticAudioTest::ExpectDisconnect();
-  }
-
-  EXPECT_TRUE(error_occurred_2_);
-  EXPECT_EQ(received_gain_db_2_, kTooLowGainDb);
-}
-
-// RendererTwoGainControlsTest
-// Renderer with two gain controls: both should receive identical notifications.
-//
-void RendererTwoGainControlsTest::SetUp() {
-  SiblingGainControlsTest::SetUp();
-
-  SetUpRenderer();
-  SetUpGainControl2OnRenderer();
-  SetUpGainControlOnRenderer();
-}
-
-TEST_F(RendererTwoGainControlsTest, BothControlsReceiveGainNotifications) { TestSetGain(); }
-
-TEST_F(RendererTwoGainControlsTest, BothControlsReceiveMuteNotifications) { TestSetMute(); }
-
-TEST_F(RendererTwoGainControlsTest, DuplicateSetGain) { TestDuplicateSetGain(); }
-
-// N.B. DuplicateSetMute behavior is tested in RendererGainControlTest.
-
-TEST_F(RendererTwoGainControlsTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-TEST_F(RendererTwoGainControlsTest, SetGainTooLow) { TestSetGainTooLow(); }
-
-TEST_F(RendererTwoGainControlsTest, SetGainNaN) { TestSetGainNaN(); }
-
-// CapturerTwoGainControlsTest
-// Capturer with two gain controls: both should receive identical notifications.
-//
-void CapturerTwoGainControlsTest::SetUp() {
-  SiblingGainControlsTest::SetUp();
-
-  SetUpCapturer();
-  SetUpGainControl2OnCapturer();
-  SetUpGainControlOnCapturer();
-}
-
-TEST_F(CapturerTwoGainControlsTest, BothControlsReceiveGainNotifications) { TestSetGain(); }
-
-TEST_F(CapturerTwoGainControlsTest, BothControlsReceiveMuteNotifications) { TestSetMute(); }
-
-// N.B. DuplicateSetGain behavior is tested in CapturerGainControlTest.
-TEST_F(CapturerTwoGainControlsTest, DuplicateSetMute) { TestDuplicateSetMute(); }
-
-TEST_F(CapturerTwoGainControlsTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-TEST_F(CapturerTwoGainControlsTest, SetGainTooLow) { TestSetGainTooLow(); }
-
-TEST_F(CapturerTwoGainControlsTest, SetGainNaN) { TestSetGainNaN(); }
-
-// IndependentGainControlsTest
-// Verify that GainControls on different API instances are fully independent.
-//
-
-// Tests expect a gain callback and no error, and neither on the independent
-// API binding and gain_control (thus we check for subsequent callback below).
-void IndependentGainControlsTest::ExpectGainCallback(float gain_db, bool mute) {
-  received_gain_db_2_ = kTooLowGainDb;
-
-  GainControlTestBase::ExpectGainCallback(gain_db, mute);
-
-  // Not only must we not have disconnected or received unexpected gain2
-  // callback, also gain1 must have received the expected callback.
-  EXPECT_EQ(received_gain_db_2_, kTooLowGainDb);
-
-  // Even if we did get the gain callback we wanted, now we check for other
-  // gain callbacks -- or a disconnect. If any of these occur, then we fail.
-  if (!error_occurred_ && received_gain_db_ == gain_db && received_gain_db_2_ == kTooLowGainDb) {
-    received_gain_db_ = kTooLowGainDb;
-
-    RunLoopUntilIdle();
-
-    EXPECT_FALSE(error_occurred_) << kDisconnectErr;
-    EXPECT_EQ(received_gain_db_, kTooLowGainDb);
-    EXPECT_EQ(received_gain_db_2_, kTooLowGainDb);
-  }
-}
-
-// Tests expect to receive a disconnect callback for the API binding, then
-// another for the GainControl binding. If before unbinding, that GainControl
-// generates a gain callback, this is unexpected and treated as an error. We
-// still expect nothing from the independent API binding and its gain_control
-// (thus we wait for timeout).
-void IndependentGainControlsTest::ExpectDisconnect() {
-  received_gain_db_2_ = kTooLowGainDb;
-
-  // We expect Renderer/Capturer AND GainControl to disconnect. Wait for both.
-  // We do NOT expect second renderer/capturer to disconnect nor other callback.
-  GainControlTestBase::ExpectDisconnect();
-
-  // Even if we did get the disconnect callbacks we wanted, now wait for other
-  // unexpected callbacks. If none occur, then we pass.
-  RunLoopUntilIdle();
-
-  // After these disconnects, both Gain and API should be gone, but not Gain2.
-  EXPECT_FALSE(error_occurred_2_) << "Unexpected disconnect: independent gain";
-  EXPECT_TRUE(gain_control_2_.is_bound());
-
-  EXPECT_EQ(received_gain_db_2_, kTooLowGainDb);
-}
-
-// TwoRenderersGainControlsTest
-// Two renderers, each with a gain control: we expect no cross-impact.
-//
-void TwoRenderersGainControlsTest::SetUp() {
-  IndependentGainControlsTest::SetUp();
-
-  SetUpRenderer2();
-  SetUpGainControl2OnRenderer2();
-
-  SetUpRenderer();
-  SetUpGainControlOnRenderer();
-}
-
-TEST_F(TwoRenderersGainControlsTest, OtherInstanceReceivesNoMuteNotification) { TestSetMute(); }
-
-// We expect primary GainControl/Renderer to disconnect.
-TEST_F(TwoRenderersGainControlsTest, SetGainTooLow) { TestSetGainTooLow(); }
-
-// RendererCapturerGainControlsTest
-// Renderer gain control should not affect capturer gain control.
-//
-void RendererCapturerGainControlsTest::SetUp() {
-  IndependentGainControlsTest::SetUp();
-
-  SetUpCapturer();
-  SetUpGainControl2OnCapturer();
-
-  SetUpRenderer();
-  SetUpGainControlOnRenderer();
-}
-
-TEST_F(RendererCapturerGainControlsTest, OtherInstanceReceivesNoGainNotification) { TestSetGain(); }
-
-// We expect primary GainControl/Renderer to disconnect.
-TEST_F(RendererCapturerGainControlsTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-// CapturerRendererGainControlsTest
-// Capturer gain control should not affect renderer gain control.
-//
-void CapturerRendererGainControlsTest::SetUp() {
-  IndependentGainControlsTest::SetUp();
-
-  SetUpRenderer();
-  SetUpGainControl2OnRenderer();
-
-  SetUpCapturer();
-  SetUpGainControlOnCapturer();
-}
-
-TEST_F(CapturerRendererGainControlsTest, OtherInstanceReceivesNoGainNotification) { TestSetGain(); }
-
-// We expect primary GainControl/Capturer to disconnect.
-TEST_F(CapturerRendererGainControlsTest, SetGainTooHigh) { TestSetGainTooHigh(); }
-
-// TwoCapturersGainControlsTest
-// Two capturers, each with a gain control: we expect no cross-impact.
-//
-void TwoCapturersGainControlsTest::SetUp() {
-  IndependentGainControlsTest::SetUp();
-
-  SetUpCapturer2();
-  SetUpGainControl2OnCapturer2();
-
-  SetUpCapturer();
-  SetUpGainControlOnCapturer();
-}
-
-TEST_F(TwoCapturersGainControlsTest, OtherInstanceReceivesNoMuteNotification) { TestSetMute(); }
-
-// We expect primary GainControl/Capturer to disconnect.
-TEST_F(TwoCapturersGainControlsTest, SetGainTooLow) { TestSetGainTooLow(); }
-
 }  // namespace media::audio::test
diff --git a/src/media/audio/audio_core/test/api/gain_control_test.h b/src/media/audio/audio_core/test/api/gain_control_test.h
deleted file mode 100644
index 5c8ad78..0000000
--- a/src/media/audio/audio_core/test/api/gain_control_test.h
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef SRC_MEDIA_AUDIO_AUDIO_CORE_TEST_FIDL_GAIN_CONTROL_TEST_H_
-#define SRC_MEDIA_AUDIO_AUDIO_CORE_TEST_FIDL_GAIN_CONTROL_TEST_H_
-
-#include <fuchsia/media/cpp/fidl.h>
-
-#include "src/media/audio/lib/test/hermetic_audio_test.h"
-
-namespace media::audio::test {
-
-// GainControlTestBase
-//
-// This set of tests verifies asynchronous usage of GainControl.
-class GainControlTestBase : public HermeticAudioTest {
- protected:
-  void TearDown() final;
-
-  void SetNegativeExpectations() override;
-  void SetUpRenderer();
-  void SetUpCapturer();
-  void SetUpRenderer2();
-  void SetUpCapturer2();
-  void SetUpGainControl();
-  void SetUpGainControlOnRenderer();
-  void SetUpGainControlOnCapturer();
-  void SetUpGainControl2();
-  void SetUpGainControl2OnRenderer();
-  void SetUpGainControl2OnCapturer();
-  void SetUpGainControl2OnRenderer2();
-  void SetUpGainControl2OnCapturer2();
-
-  // Always augmented by child implementations that set up the API interface.
-  virtual bool ApiIsNull() = 0;
-
-  void SetGain(float gain_db);
-  void SetMute(bool mute);
-
-  // Tests expect a gain callback. Absorb this; perform related error checking.
-  virtual void ExpectGainCallback(float gain_db, bool mute);
-
-  // Tests expect the API binding to disconnect, then the GainControl binding as
-  // well. After the first disconnect, assert that GainControl is still bound.
-  void ExpectDisconnect() override;
-
-  // Core test cases that are validated across various scenarios
-  void TestSetGain();
-  void TestSetMute();
-  void TestSetGainMute();
-  void TestDuplicateSetGain();
-  void TestDuplicateSetMute();
-  void TestSetGainTooHigh();
-  void TestSetGainTooLow();
-  void TestSetGainNaN();
-
-  fuchsia::media::AudioRendererPtr audio_renderer_;
-  fuchsia::media::AudioCapturerPtr audio_capturer_;
-  fuchsia::media::audio::GainControlPtr gain_control_;
-
-  float received_gain_db_ = kTooLowGainDb;
-  bool received_mute_ = false;
-
-  // Member variables for tests that use multiple interface bindings
-  bool error_occurred_2_ = false;
-  fuchsia::media::AudioRendererPtr audio_renderer_2_;
-  fuchsia::media::AudioCapturerPtr audio_capturer_2_;
-  fuchsia::media::audio::GainControlPtr gain_control_2_;
-
-  float received_gain_db_2_ = kTooLowGainDb;
-  bool received_mute_2_ = false;
-
-  // Member variables to manage our expectations
-  bool null_api_expected_ = false;
-  bool null_gain_control_expected_ = false;
-  bool null_gain_control_expected_2_ = true;
-  bool error_expected_2_ = false;
-};
-
-// RenderGainControlTest
-//
-class RenderGainControlTest : public GainControlTestBase {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_renderer_.is_bound(); }
-};
-
-// CaptureGainControlTest
-//
-class CaptureGainControlTest : public GainControlTestBase {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_capturer_.is_bound(); }
-};
-
-// SiblingGainControlsTest
-//
-// On a renderer/capturer, sibling GainControls receive identical notifications.
-class SiblingGainControlsTest : public GainControlTestBase {
- protected:
-  void SetNegativeExpectations() override;
-
-  // Absorb a gain callback from the sibling GainControl as well.
-  void ExpectGainCallback(float gain_db, bool mute) final;
-
-  // Absorb the second GainControl's disconnect, once the first disconnects.
-  void ExpectDisconnect() final;
-};
-
-// RendererTwoGainControlsTest
-//
-// Verify that Renderer's second GainControl receives the same notifications.
-class RendererTwoGainControlsTest : public SiblingGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_renderer_.is_bound(); }
-};
-
-// CapturerTwoGainControlsTest
-//
-// Verify that Capturer's second GainControl receives the same notifications.
-class CapturerTwoGainControlsTest : public SiblingGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_capturer_.is_bound(); }
-};
-
-// IndependentGainControlsTest
-//
-// Verify that GainControls on different API instances are fully independent.
-class IndependentGainControlsTest : public GainControlTestBase {
- protected:
-  // Expect nothing from the independent gain control.
-  void ExpectGainCallback(float gain_db, bool mute) final;
-
-  // Expect NO disconnect from our independent gain control -- after the first
-  // gain control disconnect has already occurred.
-  void ExpectDisconnect() final;
-};
-
-// TwoRenderersGainControlsTest
-//
-// Verify that Renderers' GainControls are fully independent.
-class TwoRenderersGainControlsTest : public IndependentGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_renderer_.is_bound() && audio_renderer_2_.is_bound(); }
-};
-
-// RendererCapturerGainControlsTest
-//
-// Verify that Renderer GainControl does not affect Capturer GainControl.
-class RendererCapturerGainControlsTest : public IndependentGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_renderer_.is_bound() && audio_capturer_.is_bound(); }
-};
-
-// CapturerRendererGainControlsTest
-//
-// Verify that Capturer GainControl does not affect Renderer GainControl.
-class CapturerRendererGainControlsTest : public IndependentGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_capturer_.is_bound() && audio_renderer_.is_bound(); }
-};
-
-// TwoCapturersGainControlsTest
-//
-// Verify that Capturers' GainControls are fully independent.
-class TwoCapturersGainControlsTest : public IndependentGainControlsTest {
- protected:
-  void SetUp() override;
-  bool ApiIsNull() final { return !audio_capturer_.is_bound() && audio_capturer_2_.is_bound(); }
-};
-
-}  // namespace media::audio::test
-
-#endif  // SRC_MEDIA_AUDIO_AUDIO_CORE_TEST_FIDL_GAIN_CONTROL_TEST_H_
diff --git a/src/media/audio/audio_core/test/api/usage_gain_reporter_test.cc b/src/media/audio/audio_core/test/api/usage_gain_reporter_test.cc
index a7573c3..58bbd1c 100644
--- a/src/media/audio/audio_core/test/api/usage_gain_reporter_test.cc
+++ b/src/media/audio/audio_core/test/api/usage_gain_reporter_test.cc
@@ -74,7 +74,7 @@
 // Test that the user is connected to the usage gain reporter.
 // TODO(50645): Also test muted
 TEST_F(UsageGainReporterTest, ConnectToUsageGainReporter) {
-  fit::closure completer = CompletionCallback([] {});
+  fit::closure completer = AddCallback("OnGainMuteChanged", [] {});
 
   // The specific choice of format doesn't matter here, any output device will do.
   constexpr auto kSampleFormat = fuchsia::media::AudioSampleFormat::SIGNED_16;
@@ -90,7 +90,7 @@
 
   fuchsia::media::UsageGainReporterPtr gain_reporter;
   environment()->ConnectToService(gain_reporter.NewRequest());
-  gain_reporter.set_error_handler(ErrorHandler());
+  AddErrorHandler(gain_reporter, "GainReporter");
 
   auto fake_listener = std::make_unique<FakeGainListener>(std::move(completer));
   gain_reporter->RegisterListener(device_id_string_, fidl::Clone(usage),
diff --git a/src/media/audio/audio_core/test/api/usage_reporter_test.cc b/src/media/audio/audio_core/test/api/usage_reporter_test.cc
index b335b42..e0eb632 100644
--- a/src/media/audio/audio_core/test/api/usage_reporter_test.cc
+++ b/src/media/audio/audio_core/test/api/usage_reporter_test.cc
@@ -35,11 +35,11 @@
 
 // TODO(50645): More complete testing of the integration with renderers
 TEST_F(UsageReporterTest, ConnectToUsageReporter) {
-  fit::closure completer = CompletionCallback([] {});
+  fit::closure completer = AddCallback("OnStateChanged", [] {});
 
   fuchsia::media::UsageReporterPtr audio_core;
   environment()->ConnectToService(audio_core.NewRequest());
-  audio_core.set_error_handler(ErrorHandler());
+  AddErrorHandler(audio_core, "AudioCore");
 
   fuchsia::media::Usage usage;
   usage.set_render_usage(fuchsia::media::AudioRenderUsage::MEDIA);
diff --git a/src/media/audio/audio_core/test/api/volume_control.cc b/src/media/audio/audio_core/test/api/volume_control.cc
index aeaadeb..788fb0c 100644
--- a/src/media/audio/audio_core/test/api/volume_control.cc
+++ b/src/media/audio/audio_core/test/api/volume_control.cc
@@ -18,7 +18,7 @@
 TEST_F(VolumeControlTest, ConnectToRenderUsageVolume) {
   fuchsia::media::AudioCorePtr audio_core;
   environment()->ConnectToService(audio_core.NewRequest());
-  audio_core.set_error_handler(ErrorHandler());
+  AddErrorHandler(audio_core, "AudioCore");
 
   fuchsia::media::audio::VolumeControlPtr client1;
   fuchsia::media::audio::VolumeControlPtr client2;
@@ -31,20 +31,25 @@
 
   float volume = 0.0;
   bool muted = false;
-  client2.events().OnVolumeMuteChanged =
-      CompletionCallback([&volume, &muted](float new_volume, bool new_muted) {
-        volume = new_volume;
-        muted = new_muted;
-      });
+  auto add_callback = [this, &client2, &volume, &muted]() {
+    client2.events().OnVolumeMuteChanged =
+        AddCallback("OnVolumeMuteChanged", [&volume, &muted](float new_volume, bool new_muted) {
+          volume = new_volume;
+          muted = new_muted;
+        });
+  };
 
+  add_callback();
   ExpectCallback();
   EXPECT_FLOAT_EQ(volume, 1.0);
 
+  add_callback();
   client1->SetVolume(0.5);
   ExpectCallback();
   EXPECT_FLOAT_EQ(volume, 0.5);
   EXPECT_EQ(muted, false);
 
+  add_callback();
   client1->SetMute(true);
   ExpectCallback();
   EXPECT_EQ(muted, true);
diff --git a/src/media/audio/audio_core/test/audio_admin/audio_admin_test.cc b/src/media/audio/audio_core/test/audio_admin/audio_admin_test.cc
index 6c36b0e..65f584b 100644
--- a/src/media/audio/audio_core/test/audio_admin/audio_admin_test.cc
+++ b/src/media/audio/audio_core/test/audio_admin/audio_admin_test.cc
@@ -201,8 +201,8 @@
   // we should have mixed audio available for capture.  Our playback is sized
   // to be much much larger than our capture to prevent test flakes.
   renderer->renderer()->Play(zx::clock::get_monotonic().get(), 0,
-                             CompletionCallback([&ref_time_received, &media_time_received](
-                                                    int64_t ref_time, int64_t media_time) {
+                             AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                     int64_t ref_time, int64_t media_time) {
                                ref_time_received = ref_time;
                                media_time_received = media_time;
                              }));
@@ -217,13 +217,11 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Add a callback for when we get our captured packet.
-  bool produced_packet = false;
   capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured, &produced_packet](fuchsia::media::StreamPacket packet) {
+      AddCallback("OnPacketProduced", [&captured](fuchsia::media::StreamPacket packet) {
         // We only care about the first set of captured samples
         if (captured.payload_size == 0) {
           captured = packet;
-          produced_packet = true;
         }
       });
 
@@ -257,17 +255,6 @@
   auto renderer = SetUpRenderer(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT, kPlaybackData1);
   auto capturer = SetUpCapturer(fuchsia::media::AudioCaptureUsage::BACKGROUND);
 
-  // Add a callback for when we get our captured packet.
-  bool produced_packet = false;
-  capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured, &produced_packet](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured.payload_size == 0) {
-          captured = packet;
-          produced_packet = true;
-        }
-      });
-
   // Get the minimum duration after submitting a packet to when we can start
   // capturing what we sent on the loopback interface
   zx_duration_t sleep_duration = GetMinLeadTime({renderer});
@@ -285,8 +272,8 @@
   // we should have mixed audio available for capture.  Our playback is sized
   // to be much much larger than our capture to prevent test flakes.
   renderer->renderer()->Play(zx::clock::get_monotonic().get(), 0,
-                             CompletionCallback([&ref_time_received, &media_time_received](
-                                                    int64_t ref_time, int64_t media_time) {
+                             AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                     int64_t ref_time, int64_t media_time) {
                                ref_time_received = ref_time;
                                media_time_received = media_time;
                              }));
@@ -301,6 +288,13 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Capture 10 samples of audio.
+  capturer->capturer().events().OnPacketProduced =
+      AddCallback("OnPacketProduced", [&captured](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured.payload_size == 0) {
+          captured = packet;
+        }
+      });
   capturer->capturer()->StartAsyncCapture(10);
   ExpectCallback();
 
@@ -352,8 +346,8 @@
   // we should have mixed audio available for capture.  Our playback is sized
   // to be much much larger than our capture to prevent test flakes.
   renderer->renderer()->Play(zx::clock::get_monotonic().get(), 0,
-                             CompletionCallback([&ref_time_received, &media_time_received](
-                                                    int64_t ref_time, int64_t media_time) {
+                             AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                     int64_t ref_time, int64_t media_time) {
                                ref_time_received = ref_time;
                                media_time_received = media_time;
                              }));
@@ -371,8 +365,8 @@
   fuchsia::media::StreamPacket loopback_captured;
   bool produced_loopback_packet = false;
   loopback_capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&loopback_captured, &produced_loopback_packet,
-                          ref_time_received](fuchsia::media::StreamPacket packet) {
+      AddCallback("OnPacketProduced", [&loopback_captured, &produced_loopback_packet,
+                                       ref_time_received](fuchsia::media::StreamPacket packet) {
         // We only care about the first set of captured samples
         if (packet.pts > ref_time_received && loopback_captured.payload_size == 0) {
           loopback_captured = packet;
@@ -411,18 +405,6 @@
   // SetUp loopback capture
   auto capturer = SetUpLoopbackCapturer();
 
-  // Add a callback for when we get our captured packet.
-  bool produced_packet = false;
-  fuchsia::media::StreamPacket captured;
-  capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured, &produced_packet](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured.payload_size == 0) {
-          captured = packet;
-          produced_packet = true;
-        }
-      });
-
   // Get the minimum duration after submitting a packet to when we can start
   // capturing what we sent on the loopback interface.
   zx_duration_t sleep_duration = GetMinLeadTime({renderer1, renderer2});
@@ -445,8 +427,8 @@
   renderer1->renderer()->PlayNoReply(playat, 0);
   // Only get the callback for the second renderer.
   renderer2->renderer()->Play(playat, 0,
-                              CompletionCallback([&ref_time_received, &media_time_received](
-                                                     int64_t ref_time, int64_t media_time) {
+                              AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                      int64_t ref_time, int64_t media_time) {
                                 ref_time_received = ref_time;
                                 media_time_received = media_time;
                               }));
@@ -461,6 +443,14 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Capture 10 samples of audio.
+  fuchsia::media::StreamPacket captured;
+  capturer->capturer().events().OnPacketProduced =
+      AddCallback("OnPacketProduced", [&captured](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured.payload_size == 0) {
+          captured = packet;
+        }
+      });
   capturer->capturer()->StartAsyncCapture(10);
   ExpectCallback();
 
@@ -504,18 +494,6 @@
   // SetUp loopback capture
   auto capturer = SetUpLoopbackCapturer();
 
-  // Add a callback for when we get our captured packet.
-  bool produced_packet = false;
-  fuchsia::media::StreamPacket captured;
-  capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured, &produced_packet](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured.payload_size == 0) {
-          captured = packet;
-          produced_packet = true;
-        }
-      });
-
   // Get the minimum duration after submitting a packet to when we can start
   // capturing what we sent on the loopback interface.
   zx_duration_t sleep_duration = GetMinLeadTime({renderer1, renderer2});
@@ -538,8 +516,8 @@
   renderer1->renderer()->PlayNoReply(playat, 0);
   // Only get the callback for the second renderer.
   renderer2->renderer()->Play(playat, 0,
-                              CompletionCallback([&ref_time_received, &media_time_received](
-                                                     int64_t ref_time, int64_t media_time) {
+                              AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                      int64_t ref_time, int64_t media_time) {
                                 ref_time_received = ref_time;
                                 media_time_received = media_time;
                               }));
@@ -554,6 +532,14 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Capture 10 samples of audio.
+  fuchsia::media::StreamPacket captured;
+  capturer->capturer().events().OnPacketProduced =
+      AddCallback("OnPacketProduced", [&captured](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured.payload_size == 0) {
+          captured = packet;
+        }
+      });
   capturer->capturer()->StartAsyncCapture(10);
   ExpectCallback();
 
@@ -584,18 +570,6 @@
   // SetUp loopback capture
   auto capturer = SetUpLoopbackCapturer();
 
-  // Add a callback for when we get our captured packet.
-  bool produced_packet = false;
-  fuchsia::media::StreamPacket captured;
-  capturer->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured, &produced_packet](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured.payload_size == 0) {
-          captured = packet;
-          produced_packet = true;
-        }
-      });
-
   // Get the minimum duration after submitting a packet to when we can start
   // capturing what we sent on the loopback interface.
   zx_duration_t sleep_duration = GetMinLeadTime({renderer1});
@@ -618,8 +592,8 @@
   renderer1->renderer()->PlayNoReply(playat, 0);
   // Only get the callback for the second renderer.
   renderer2->renderer()->Play(playat, 0,
-                              CompletionCallback([&ref_time_received, &media_time_received](
-                                                     int64_t ref_time, int64_t media_time) {
+                              AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                      int64_t ref_time, int64_t media_time) {
                                 ref_time_received = ref_time;
                                 media_time_received = media_time;
                               }));
@@ -634,6 +608,14 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Capture 10 samples of audio.
+  fuchsia::media::StreamPacket captured;
+  capturer->capturer().events().OnPacketProduced =
+      AddCallback("OnPacketProduced", [&captured](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured.payload_size == 0) {
+          captured = packet;
+        }
+      });
   capturer->capturer()->StartAsyncCapture(10);
   ExpectCallback();
 
@@ -663,29 +645,6 @@
   auto capturer1 = SetUpCapturer(fuchsia::media::AudioCaptureUsage::BACKGROUND);
   auto capturer2 = SetUpCapturer(fuchsia::media::AudioCaptureUsage::BACKGROUND);
 
-  // Add a callback for when we get our captured packet.
-  fuchsia::media::StreamPacket captured1;
-  bool produced_packet1 = false;
-  capturer1->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured1, &produced_packet1](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured1.payload_size == 0) {
-          captured1 = packet;
-          produced_packet1 = true;
-        }
-      });
-
-  fuchsia::media::StreamPacket captured2;
-  bool produced_packet2 = false;
-  capturer2->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured2, &produced_packet2](fuchsia::media::StreamPacket packet) {
-        // We only care about the first set of captured samples
-        if (captured2.payload_size == 0) {
-          captured2 = packet;
-          produced_packet2 = true;
-        }
-      });
-
   // Get the minimum duration after submitting a packet to when we can start
   // capturing what we sent on the loopback interface.
   zx_duration_t sleep_duration = GetMinLeadTime({renderer});
@@ -706,8 +665,8 @@
   // to be much much larger than our capture to prevent test flakes.
   auto playat = zx::clock::get_monotonic().get();
   renderer->renderer()->Play(playat, 0,
-                             CompletionCallback([&ref_time_received, &media_time_received](
-                                                    int64_t ref_time, int64_t media_time) {
+                             AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                     int64_t ref_time, int64_t media_time) {
                                ref_time_received = ref_time;
                                media_time_received = media_time;
                              }));
@@ -722,10 +681,27 @@
   zx_nanosleep(zx_deadline_after(sleep_duration));
 
   // Capture 10 samples of audio.
+  fuchsia::media::StreamPacket captured1;
+  capturer1->capturer().events().OnPacketProduced =
+      AddCallbackUnordered("OnPacketProduced", [&captured1](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured1.payload_size == 0) {
+          captured1 = packet;
+        }
+      });
+
+  fuchsia::media::StreamPacket captured2;
+  capturer2->capturer().events().OnPacketProduced =
+      AddCallbackUnordered("OnPacketProduced", [&captured2](fuchsia::media::StreamPacket packet) {
+        // We only care about the first set of captured samples
+        if (captured2.payload_size == 0) {
+          captured2 = packet;
+        }
+      });
+
   capturer1->capturer()->StartAsyncCapture(10);
   capturer2->capturer()->StartAsyncCapture(10);
-  RunLoopUntil(
-      [&produced_packet1, &produced_packet2]() { return produced_packet1 && produced_packet2; });
+  ExpectCallback();
 
   // Check that all of the samples contain the expected data.
   ExpectPacketContains("captured1", captured1, capturer1->SnapshotPayload(), 10,
@@ -759,8 +735,8 @@
   // Add a callback for when we get our captured packet.
   fuchsia::media::StreamPacket captured1;
   bool produced_packet1 = false;
-  capturer1->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured1, &produced_packet1](fuchsia::media::StreamPacket packet) {
+  capturer1->capturer().events().OnPacketProduced = AddCallback(
+      "OnPacketProduced", [&captured1, &produced_packet1](fuchsia::media::StreamPacket packet) {
         // We only care about the first set of captured samples
         if (captured1.payload_size == 0) {
           captured1 = packet;
@@ -770,8 +746,8 @@
 
   fuchsia::media::StreamPacket captured2;
   bool produced_packet2 = false;
-  capturer2->capturer().events().OnPacketProduced =
-      CompletionCallback([&captured2, &produced_packet2](fuchsia::media::StreamPacket packet) {
+  capturer2->capturer().events().OnPacketProduced = AddCallback(
+      "OnPacketProduced", [&captured2, &produced_packet2](fuchsia::media::StreamPacket packet) {
         // We only care about the first set of captured samples
         if (captured2.payload_size == 0) {
           captured2 = packet;
@@ -799,8 +775,8 @@
   // to be much much larger than our capture to prevent test flakes.
   auto playat = zx::clock::get_monotonic().get();
   renderer->renderer()->Play(playat, 0,
-                             CompletionCallback([&ref_time_received, &media_time_received](
-                                                    int64_t ref_time, int64_t media_time) {
+                             AddCallback("Play", [&ref_time_received, &media_time_received](
+                                                     int64_t ref_time, int64_t media_time) {
                                ref_time_received = ref_time;
                                media_time_received = media_time;
                              }));
diff --git a/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.cc b/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.cc
index e14ff3b..f73200a 100644
--- a/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.cc
+++ b/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.cc
@@ -12,6 +12,14 @@
 
 namespace media::audio::test {
 
+// For operations expected to generate a response, wait __1 minute__. We do this to avoid flaky
+// results when testing on high-load (high-latency) environments. For reference, in mid-2018 when
+// observing highly-loaded local QEMU instances running code that generated correct completion
+// responses, we observed timeouts if waiting 20 ms, but not if waiting 50 ms. This value is 3000x
+// that (!) -- WELL beyond the limit of human acceptability. Thus, intermittent failures (rather
+// than being a "potentially flaky test") mean that the system is, intermittently, UNACCEPTABLE.
+constexpr zx::duration kDurationResponseExpected = zx::sec(60);
+
 void AudioCoreHardwareTest::SetUp() {
   TestFixture::SetUp();
 
@@ -29,16 +37,11 @@
   RunLoopUntilIdle();
 }
 
-void AudioCoreHardwareTest::TearDown() { ASSERT_FALSE(error_occurred()); }
-
 bool AudioCoreHardwareTest::WaitForCaptureDevice() {
   audio_device_enumerator_ = sys::ServiceDirectory::CreateFromNamespace()
                                  ->Connect<fuchsia::media::AudioDeviceEnumerator>();
 
-  audio_device_enumerator_.set_error_handler(ErrorHandler([](zx_status_t status) {
-    FAIL() << "Client connection to fuchsia.media.AudioDeviceEnumerator: "
-           << zx_status_get_string(status) << " (" << status << ")";
-  }));
+  AddErrorHandler(audio_device_enumerator_, "AudioDeviceEnumerator");
 
   audio_device_enumerator_.events().OnDeviceAdded =
       ([this](fuchsia::media::AudioDeviceInfo device) {
@@ -78,18 +81,14 @@
         }
       });
 
-  RunLoopWithTimeoutOrUntil([this]() { return error_occurred_ || capture_device_is_default_; },
+  RunLoopWithTimeoutOrUntil([this]() { return ErrorOccurred() || capture_device_is_default_; },
                             kDurationResponseExpected);
   return capture_device_is_default_;
 }
 
 void AudioCoreHardwareTest::ConnectToAudioCore() {
   audio_core_ = sys::ServiceDirectory::CreateFromNamespace()->Connect<fuchsia::media::AudioCore>();
-
-  audio_core_.set_error_handler(ErrorHandler([](zx_status_t status) {
-    FAIL() << "Client connection to fuchsia.media.AudioCore: " << zx_status_get_string(status)
-           << " (" << status << ")";
-  }));
+  AddErrorHandler(audio_core_, "AudioCore");
 }
 
 void AudioCoreHardwareTest::ConnectToAudioCapturer() {
@@ -97,11 +96,7 @@
 
   constexpr bool kNotLoopback = false;
   audio_core_->CreateAudioCapturer(kNotLoopback, audio_capturer_.NewRequest());
-
-  audio_capturer_.set_error_handler(ErrorHandler([](zx_status_t status) {
-    FAIL() << "Client connection to fuchsia.media.AudioCapturer: " << zx_status_get_string(status)
-           << " (" << status << ")";
-  }));
+  AddErrorHandler(audio_capturer_, "AudioCapturer");
 
   audio_capturer_->SetUsage(kUsage);
 }
@@ -110,11 +105,7 @@
   ASSERT_TRUE(audio_capturer_.is_bound());
 
   audio_capturer_->BindGainControl(stream_gain_control_.NewRequest());
-
-  stream_gain_control_.set_error_handler(ErrorHandler([](zx_status_t status) {
-    FAIL() << "Client connection to (capture stream) fuchsia.media.audio.GainControl: "
-           << zx_status_get_string(status) << " (" << status << ")";
-  }));
+  AddErrorHandler(stream_gain_control_, "AudioCapturer::GainControl");
 }
 
 // Set gain for this capturer gain control, capture usage and all capture devices.
@@ -133,13 +124,14 @@
 
 // Fetch the initial media type and adjust channel_count_ and frames_per_second_ if needed.
 void AudioCoreHardwareTest::GetDefaultCaptureFormat() {
-  audio_capturer_->GetStreamType(CompletionCallback([this](fuchsia::media::StreamType stream_type) {
-    ASSERT_TRUE(stream_type.medium_specific.is_audio()) << "Default format is not audio!";
-    const auto& format = stream_type.medium_specific.audio();
+  audio_capturer_->GetStreamType(
+      AddCallback("GetStreamType", [this](fuchsia::media::StreamType stream_type) {
+        ASSERT_TRUE(stream_type.medium_specific.is_audio()) << "Default format is not audio!";
+        const auto& format = stream_type.medium_specific.audio();
 
-    channel_count_ = format.channels;
-    frames_per_second_ = format.frames_per_second;
-  }));
+        channel_count_ = format.channels;
+        frames_per_second_ = format.frames_per_second;
+      }));
 
   ExpectCallback();
 
@@ -213,7 +205,7 @@
   const uint32_t payload_offset = 0u;
 
   audio_capturer_->CaptureAt(kPayloadBufferId, payload_offset, vmo_buffer_frame_count_,
-                             CompletionCallback([this](fuchsia::media::StreamPacket packet) {
+                             AddCallback("CaptureAt", [this](fuchsia::media::StreamPacket packet) {
                                OnPacketProduced(packet);
                              }));
   // Wait for the capture buffer to be returned.
diff --git a/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.h b/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.h
index b7b0d52..c625325 100644
--- a/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.h
+++ b/src/media/audio/audio_core/test/hardware/audio_core_hardware_test.h
@@ -36,7 +36,6 @@
   static constexpr uint32_t kBytesPerSample = 4;
 
   void SetUp() override;
-  void TearDown() override;
 
   bool WaitForCaptureDevice();
 
diff --git a/src/media/audio/audio_core/test/pipeline/audio_pipeline_test.cc b/src/media/audio/audio_core/test/pipeline/audio_pipeline_test.cc
index 4d8f3cc..680addd 100644
--- a/src/media/audio/audio_core/test/pipeline/audio_pipeline_test.cc
+++ b/src/media/audio/audio_core/test/pipeline/audio_pipeline_test.cc
@@ -122,14 +122,9 @@
   renderer_->Play(this, first_time, 0);
   renderer_->WaitForPackets(this, first_time, {first_packets[0], first_packets[1]});
 
-  auto received_discard_all_callback = false;
-  renderer_->renderer()->DiscardAllPackets(CompletionCallback([&received_discard_all_callback]() {
-    received_discard_all_callback = true;
-    AUDIO_LOG(DEBUG) << "DiscardAllPackets #1 complete";
-  }));
-  RunLoopUntil([this, &received_discard_all_callback]() {
-    return (error_occurred_ || received_discard_all_callback);
-  });
+  renderer_->renderer()->DiscardAllPackets(AddCallback(
+      "DiscardAllPackets", []() { AUDIO_LOG(DEBUG) << "DiscardAllPackets #1 complete"; }));
+  ExpectCallback();
 
   // The entire first two packets must have been written. Subsequent packets may have been partially
   // written, depending on exactly when the DiscardAllPackets command is received. The remaining
diff --git a/src/media/audio/lib/test/BUILD.gn b/src/media/audio/lib/test/BUILD.gn
index 17020f0..15fad1a 100644
--- a/src/media/audio/lib/test/BUILD.gn
+++ b/src/media/audio/lib/test/BUILD.gn
@@ -27,6 +27,7 @@
 
   public_deps = [
     "//garnet/public/lib/gtest",
+    "//sdk/lib/fidl/cpp",
     "//zircon/public/lib/fit",
   ]
 }
diff --git a/src/media/audio/lib/test/capturer_shim.h b/src/media/audio/lib/test/capturer_shim.h
index 21e6a25..c18aebc 100644
--- a/src/media/audio/lib/test/capturer_shim.h
+++ b/src/media/audio/lib/test/capturer_shim.h
@@ -76,7 +76,7 @@
       : CapturerShimImpl(format, payload_frame_count) {
     audio_core->CreateAudioCapturerWithConfiguration(format.stream_type(), std::move(config),
                                                      capturer_.NewRequest());
-    capturer_.set_error_handler(fixture->ErrorHandler());
+    fixture->AddErrorHandler(capturer_, "AudioCapturer");
 
     capturer_->SetPcmStreamType({.sample_format = format_.sample_format(),
                                  .channels = format_.channels(),
@@ -114,11 +114,11 @@
           // TODO(55243): Enable AddPayloadBuffer before the capturer is created.
           capturer_->AddPayloadBuffer(0, std::move(vmo));
         });
-    capturer_.set_error_handler(fixture->ErrorHandler());
+    fixture->AddErrorHandler(capturer_, "UltrasoundCapturer");
   }
 
   void WaitForDevice() {
-    fixture_->RunLoopUntil([this] { return created_ || fixture_->error_occurred(); });
+    fixture_->RunLoopUntil([this] { return created_ || fixture_->ErrorOccurred(); });
   }
 
   bool created() const { return created_; }
diff --git a/src/media/audio/lib/test/hermetic_audio_test.cc b/src/media/audio/lib/test/hermetic_audio_test.cc
index 495aeef..e1524fc 100644
--- a/src/media/audio/lib/test/hermetic_audio_test.cc
+++ b/src/media/audio/lib/test/hermetic_audio_test.cc
@@ -50,13 +50,13 @@
   TestFixture::SetUp();
 
   environment_->ConnectToService(audio_core_.NewRequest());
-  audio_core_.set_error_handler(ErrorHandler());
+  AddErrorHandler(audio_core_, "AudioCore");
 
   environment_->ConnectToService(ultrasound_factory_.NewRequest());
-  ultrasound_factory_.set_error_handler(ErrorHandler());
+  AddErrorHandler(ultrasound_factory_, "UltrasoundFactory");
 
   environment_->ConnectToService(audio_dev_enum_.NewRequest());
-  audio_dev_enum_.set_error_handler(ErrorHandler());
+  AddErrorHandler(audio_dev_enum_, "AudioDeviceEnumerator");
   WatchForDeviceArrivals();
 }
 
@@ -118,18 +118,6 @@
   TestFixture::TearDown();
 }
 
-void HermeticAudioTest::ExpectCallback() {
-  TestFixture::ExpectCallback();
-
-  EXPECT_TRUE(audio_core_.is_bound());
-}
-
-void HermeticAudioTest::ExpectDisconnect() {
-  TestFixture::ExpectDisconnect();
-
-  EXPECT_TRUE(audio_core_.is_bound());
-}
-
 template <fuchsia::media::AudioSampleFormat SampleFormat>
 VirtualOutput<SampleFormat>* HermeticAudioTest::CreateOutput(
     const audio_stream_unique_id_t& device_id, TypedFormat<SampleFormat> format,
@@ -165,7 +153,7 @@
 
   // Wait for device to become the default.
   RunLoopUntil([this, id]() { return devices_[id].is_default; });
-  EXPECT_FALSE(error_occurred_);
+  ExpectNoUnexpectedErrors("during CreateOutput");
   return out;
 }
 
@@ -197,7 +185,7 @@
   renderers_.push_back(std::move(ptr));
 
   // Wait until the renderer is connected.
-  RunLoopUntil([this, out]() { return error_occurred_ || (out->GetMinLeadTime() > 0); });
+  RunLoopUntil([this, out]() { return ErrorOccurred() || (out->GetMinLeadTime() > 0); });
   return out;
 }
 
diff --git a/src/media/audio/lib/test/hermetic_audio_test.h b/src/media/audio/lib/test/hermetic_audio_test.h
index adf27a4..df20c9e 100644
--- a/src/media/audio/lib/test/hermetic_audio_test.h
+++ b/src/media/audio/lib/test/hermetic_audio_test.h
@@ -44,9 +44,6 @@
   void SetUp() override;
   void TearDown() override;
 
-  void ExpectCallback() override;
-  void ExpectDisconnect() override;
-
   // Register that the test expects no audio underflows. This expectation will be checked by
   // TearDown().
   void FailUponUnderflows() { disallow_underflows_ = true; }
diff --git a/src/media/audio/lib/test/renderer_shim.cc b/src/media/audio/lib/test/renderer_shim.cc
index 0058650..aaf5b9e 100644
--- a/src/media/audio/lib/test/renderer_shim.cc
+++ b/src/media/audio/lib/test/renderer_shim.cc
@@ -39,12 +39,8 @@
 }
 
 void RendererShimImpl::Play(TestFixture* fixture, int64_t reference_time, int64_t media_time) {
-  bool played = false;
-  renderer_->Play(reference_time, media_time,
-                  [&played](int64_t reference_time, int64_t media_time) { played = true; });
-
-  fixture->RunLoopUntil([&played]() { return played; });
-  ASSERT_FALSE(fixture->error_occurred());
+  renderer_->Play(reference_time, media_time, fixture->AddCallback("Play"));
+  fixture->ExpectCallback();
 }
 
 template <fuchsia::media::AudioSampleFormat SampleFormat>
@@ -114,7 +110,7 @@
     }
     return true;
   });
-  ASSERT_FALSE(fixture->error_occurred());
+  fixture->ExpectNoUnexpectedErrors("during WaitForPackets");
 }
 
 // Explicitly instantiate all possible implementations.
diff --git a/src/media/audio/lib/test/renderer_shim.h b/src/media/audio/lib/test/renderer_shim.h
index 3ce682a..02c83f1 100644
--- a/src/media/audio/lib/test/renderer_shim.h
+++ b/src/media/audio/lib/test/renderer_shim.h
@@ -11,6 +11,7 @@
 #include <memory>
 #include <vector>
 
+#include "src/lib/fxl/strings/string_printf.h"
 #include "src/lib/testing/loop_fixture/real_loop_fixture.h"
 #include "src/media/audio/lib/format/audio_buffer.h"
 #include "src/media/audio/lib/format/format.h"
@@ -125,7 +126,7 @@
                     size_t payload_frame_count, fuchsia::media::AudioRenderUsage usage)
       : RendererShimImpl(format, payload_frame_count) {
     audio_core->CreateAudioRenderer(renderer_.NewRequest());
-    renderer_.set_error_handler(fixture->ErrorHandler());
+    fixture->AddErrorHandler(renderer_, "AudioRenderer");
     WatchEvents();
 
     renderer_->SetUsage(usage);
@@ -163,7 +164,7 @@
           EXPECT_EQ(stream_type.channels, format_.channels());
           EXPECT_EQ(stream_type.frames_per_second, format_.frames_per_second());
         });
-    renderer_.set_error_handler(fixture->ErrorHandler());
+    fixture->AddErrorHandler(renderer_, "UltrasoundRenderer");
 
     WatchEvents();
     SetPtsUnits(format_.frames_per_second(), 1);
@@ -171,7 +172,7 @@
   }
 
   void WaitForDevice() {
-    fixture_->RunLoopUntil([this] { return created_ || fixture_->error_occurred(); });
+    fixture_->RunLoopUntil([this] { return created_ || fixture_->ErrorOccurred(); });
   }
 
   bool created() const { return created_; }
diff --git a/src/media/audio/lib/test/test_fixture.cc b/src/media/audio/lib/test/test_fixture.cc
index eac4be8..cf8b61f 100644
--- a/src/media/audio/lib/test/test_fixture.cc
+++ b/src/media/audio/lib/test/test_fixture.cc
@@ -4,36 +4,107 @@
 
 #include "src/media/audio/lib/test/test_fixture.h"
 
+#include <zircon/status.h>
+
+#include "src/lib/fxl/strings/string_printf.h"
+
 namespace media::audio::test {
-//
-// TestFixture implementation
-//
-void TestFixture::SetUp() { ::gtest::RealLoopFixture::SetUp(); }
+
+constexpr zx::duration kLoopTimeout = zx::sec(10);
 
 void TestFixture::TearDown() {
-  EXPECT_EQ(error_expected_, error_occurred_);
-
+  ExpectNoUnexpectedErrors("during TearDown");
   ::gtest::RealLoopFixture::TearDown();
 }
 
 void TestFixture::ExpectCallback() {
-  callback_received_ = false;
+  int64_t last_seqno = 0;
+  while (!pending_callbacks_.empty()) {
+    auto pcb = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
 
-  RunLoopUntil([this]() { return (error_occurred_ || callback_received_); });
+    RunLoopWithTimeoutOrUntil([this, pcb]() { return new_error_ || pcb->seqno > 0; }, kLoopTimeout);
 
-  EXPECT_FALSE(error_occurred_) << kDisconnectErr;
-  EXPECT_TRUE(callback_received_);
+    if (new_error_) {
+      new_error_ = false;
+      ADD_FAILURE() << "Unexpected error while waiting for " << pcb->name;
+      ExpectNoUnexpectedErrors(fxl::StringPrintf("while waiting for %s", pcb->name.c_str()));
+      pending_callbacks_.clear();
+      return;
+    }
+    if (pcb->seqno == 0) {
+      ADD_FAILURE() << "Did not get a " << pcb->name << " callback within "
+                    << kLoopTimeout.to_msecs() << "ms";
+      pending_callbacks_.clear();
+      return;
+    }
+
+    if (pcb->ordered) {
+      EXPECT_GT(pcb->seqno, last_seqno) << pcb->name << " called out-of-order";
+      last_seqno = pcb->seqno;
+    }
+  }
 }
 
-void TestFixture::ExpectError(zx_status_t expected_error) {
-  SetNegativeExpectations();
-  callback_received_ = false;
+void TestFixture::ExpectErrors(const std::vector<std::shared_ptr<ErrorHandler>>& errors) {
+  std::string names = "{";
+  std::string sep;
+  for (auto& eh : errors) {
+    names += eh->name;
+    sep = ", ";
+  }
+  names += "}";
 
-  RunLoopUntil([this]() { return (error_occurred_ || callback_received_); });
+  RunLoopWithTimeoutOrUntil(
+      [errors]() {
+        for (auto& eh : errors) {
+          if (eh->error_code != eh->expected_error_code) {
+            return false;
+          }
+        }
+        return true;
+      },
+      kLoopTimeout);
 
-  EXPECT_TRUE(error_occurred_);
-  EXPECT_EQ(error_code_, expected_error);
-  EXPECT_FALSE(callback_received_) << kCallbackErr;
+  new_error_ = false;
+  ExpectNoUnexpectedErrors(fxl::StringPrintf("when waiting error in %s", names.c_str()));
+}
+
+void TestFixture::ExpectNoUnexpectedErrors(const std::string& msg_for_failure) {
+  for (auto& [_, eh] : error_handlers_) {
+    EXPECT_EQ(eh->error_code, eh->expected_error_code)
+        << msg_for_failure << ": " << eh->name << " had an unexpected error\nExpected error is "
+        << zx_status_get_string(eh->expected_error_code) << "\nActual error is "
+        << zx_status_get_string(eh->error_code);
+  }
+}
+
+std::pair<std::shared_ptr<TestFixture::ErrorHandler>, fit::function<void(zx_status_t)>>
+TestFixture::NewErrorHandler(const std::string& name) {
+  auto eh = std::make_shared<ErrorHandler>();
+  eh->name = name;
+  return std::make_pair(eh, [this, eh](zx_status_t status) {
+    eh->error_code = status;
+    new_error_ = true;
+  });
+}
+
+std::shared_ptr<TestFixture::PendingCallback> TestFixture::NewPendingCallback(
+    const std::string& name, bool ordered) {
+  auto pcb = std::make_shared<PendingCallback>();
+  pcb->name = name;
+  pcb->ordered = ordered;
+  pending_callbacks_.push_back(pcb);
+  return pcb;
+}
+
+bool TestFixture::ErrorOccurred() {
+  for (auto& [_, eh] : error_handlers_) {
+    if (eh->error_code != ZX_OK) {
+      return true;
+    }
+  }
+  return false;
 }
 
 }  // namespace media::audio::test
diff --git a/src/media/audio/lib/test/test_fixture.h b/src/media/audio/lib/test/test_fixture.h
index 04877de..495bbac 100644
--- a/src/media/audio/lib/test/test_fixture.h
+++ b/src/media/audio/lib/test/test_fixture.h
@@ -4,80 +4,126 @@
 #ifndef SRC_MEDIA_AUDIO_LIB_TEST_TEST_FIXTURE_H_
 #define SRC_MEDIA_AUDIO_LIB_TEST_TEST_FIXTURE_H_
 
+#include <lib/fidl/cpp/interface_ptr.h>
+#include <lib/fidl/cpp/synchronous_interface_ptr.h>
 #include <lib/fit/function.h>
 #include <lib/gtest/real_loop_fixture.h>
+#include <zircon/errors.h>
 
+#include <deque>
+#include <initializer_list>
 #include <optional>
+#include <unordered_map>
 
 namespace media::audio::test {
 
-// For operations expected to generate a response, wait __1 minute__. We do this to avoid flaky
-// results when testing on high-load (high-latency) environments. For reference, in mid-2018 when
-// observing highly-loaded local QEMU instances running code that generated correct completion
-// responses, we observed timeouts if waiting 20 ms, but not if waiting 50 ms. This value is 3000x
-// that (!) -- WELL beyond the limit of human acceptability. Thus, intermittent failures (rather
-// than being a "potentially flaky test") mean that the system is, intermittently, UNACCEPTABLE.
+// TestFixture wraps a RealLoopFixture with methods to check for FIDL errors and callbacks.
+// For example, to check for disconnection:
 //
-// Also, when expecting a response we can save time by checking more frequently. Restated,
-// kDurationResponseExpected should ALWAYS use kDurationGranularity.
+//     SomeInterfacePtr ptr;
+//     environment->ConnectToService(ptr.NewRequest());
+//     AddErrorHandler(ptr, "SomeInterface");
 //
-// These two values codify the following ordered priorities:
-//   1) False-positive test failures are expensive and must be eliminated.
-//   2) Having done that, streamline test run-time (time=resources=cost);
-constexpr zx::duration kDurationResponseExpected = zx::sec(60);
-constexpr zx::duration kDurationGranularity = zx::duration::infinite();
-
-constexpr char kDisconnectErr[] = "Connection to fuchsia.media FIDL interface was lost!\n";
-constexpr char kTimeoutErr[] = "Timeout -- no callback received!\n";
-constexpr char kCallbackErr[] = "Unexpected callback received!\n";
-
+//     ... do something that should disconnect ptr ...
 //
-// TestFixture
+//     ExpectDisconnect(ptr);
+//
+// Or, to check that a sequence of callbacks are executed as expected:
+//
+//     SomeInterfacePtr ptr;
+//     environment->ConnectToService(ptr.NewRequest());
+//     AddErrorHandler(ptr, "SomeInterface");
+//
+//     int b;
+//     ptr.events().OnA = AddCallback("A")
+//     ptr.events().OnB = AddCallback("B", [&b](int x) { b = x; });
+//
+//     // This verifies that callbacks A and B are executed, in that order, that B
+//     // is called with the correct argument, and that the ErrorHandler is not called.
+//     ExpectCallback();
+//     EXPECT_EQ(b, 42);
 //
 class TestFixture : public ::gtest::RealLoopFixture {
  public:
-  bool error_occurred() const { return error_occurred_; }
+  struct ErrorHandler {
+    std::string name;
+    zx_status_t error_code = ZX_OK;           // set after the ErrorHandler is triggered
+    zx_status_t expected_error_code = ZX_OK;  // expected error for ExpectErrors
+  };
 
-  // Simple handler, when the only required response is to record the error.
-  auto ErrorHandler() {
-    return [this](zx_status_t error) {
-      error_occurred_ = true;
-      error_code_ = error;
-    };
+  // Add a new ErrorHandler for the given protocol. If this ErrorHandler triggers unexpectedly,
+  // the given name will be included in the test failure message. The InterfacePtr must
+  // live for the duration of this TestFixture.
+  template <class T>
+  void AddErrorHandler(fidl::InterfacePtr<T>& ptr, std::string name) {
+    auto [h, cb] = NewErrorHandler(name);
+    ptr.set_error_handler(std::move(cb));
+    error_handlers_[ptr.channel().get()] = h;
   }
 
-  // Accept (and call) a custom handler, for more nuanced error responses.
+  // Retrieves a previously-added error handler.
+  // Useful for direct calls to ExpectErrors or ExpectDisconnects. Tests that
+  // use ExpectError or ExpectDisconnect won't need this.
+  template <class T>
+  std::shared_ptr<ErrorHandler> ErrorHandlerFor(fidl::InterfacePtr<T>& ptr) {
+    auto eh = error_handlers_[ptr.channel().get()];
+    FX_CHECK(eh);
+    return eh;
+  }
+
+  // Add an expected callback to the pending set.
+  // Callbacks are expected to occur in the order in which they are added.
+  // Optionally, providew a custom function to invoke when the expected callback is triggered.
+  auto AddCallback(const std::string& name);
   template <typename Callable>
-  auto ErrorHandler(Callable err_handler) {
-    return [this, err_handler = std::move(err_handler)](zx_status_t error) {
-      error_occurred_ = true;
-      error_code_ = error;
-      err_handler(error);
-    };
-  }
+  auto AddCallback(const std::string& name, Callable callback);
 
-  // Simple callback, when the only requirement is to record the callback.
-  auto CompletionCallback() {
-    return [this]() { callback_received_ = true; };
-  }
-
-  // Accept (and call) a custom callback, for more nuanced behavior.
+  // Like AddCallback, but allow the callback to happen in any order.
+  auto AddCallbackUnordered(const std::string& name);
   template <typename Callable>
-  auto CompletionCallback(Callable callback) {
-    return [this, callback = std::move(callback)](auto&&... args) {
-      callback_received_ = true;
-      callback(std::forward<decltype(args)>(args)...);
-    };
+  auto AddCallbackUnordered(const std::string& name, Callable callback);
+
+  // Add an unexpected callback. The test will fail if this callback is triggered.
+  auto AddUnexpectedCallback(const std::string& name) {
+    return [name](auto&&...) { ADD_FAILURE() << "Got unexpected callback " << name; };
   }
 
-  // The below methods contain gtest EXPECT checks that verify basic outcomes.
-  //
-  // Wait for CompletionCallback or ErrorHandler, expecting callback.
-  virtual void ExpectCallback();
+  // Wait until all pending callbacks are drained. Fails if an error is encountered.
+  // Callbacks are expected to occur in the order they are added. After this method
+  // returns, the pending callback set is emptied and new callbacks may be added for
+  // a future call to ExpectCallback.
+  void ExpectCallback();
 
-  // Wait for CompletionCallback or ErrorHandler, expecting the specified error.
-  virtual void ExpectDisconnect() { ExpectError(ZX_ERR_PEER_CLOSED); }
-  void ExpectError(zx_status_t expect_error);
+  // Wait for the given ErrorHandlers to trigger with their expected errors. Fails if
+  // different errors are found or if errors are triggered in different ErrorHandlers.
+  void ExpectErrors(const std::vector<std::shared_ptr<ErrorHandler>>& errors);
+
+  // Shorthand to expect many disconnect errors.
+  void ExpectDisconnects(const std::vector<std::shared_ptr<ErrorHandler>>& errors) {
+    std::vector<std::shared_ptr<ErrorHandler>> handlers;
+    for (auto eh : errors) {
+      eh->expected_error_code = ZX_ERR_PEER_CLOSED;
+    }
+    ExpectErrors(errors);
+  }
+
+  // Shorthand to expect a single error.
+  template <class T>
+  void ExpectError(fidl::InterfacePtr<T>& ptr, zx_status_t expected_error) {
+    auto eh = ErrorHandlerFor(ptr);
+    eh->expected_error_code = expected_error;
+    ExpectErrors({eh});
+  }
+  template <class T>
+  void ExpectDisconnect(fidl::InterfacePtr<T>& ptr) {
+    ExpectError(ptr, ZX_ERR_PEER_CLOSED);
+  }
+
+  // Verifies that no unexpected errors have occurred so far.
+  void ExpectNoUnexpectedErrors(const std::string& msg_for_failure);
+
+  // Reports whether any ErrorHandlers have triggered.
+  bool ErrorOccurred();
 
   // Promote to public so that non-subclasses can advance through time.
   using ::gtest::RealLoopFixture::RunLoop;
@@ -87,20 +133,61 @@
   using ::gtest::RealLoopFixture::RunLoopWithTimeoutOrUntil;
 
  protected:
-  void SetUp() override;
   void TearDown() override;
 
-  // Set expectations for negative test cases. Called by ExpectError/Disconnect.
-  virtual void SetNegativeExpectations() { error_expected_ = true; }
-
-  bool error_expected_ = false;
-  bool error_occurred_ = false;
-  zx_status_t error_code_ = ZX_OK;
-
  private:
-  bool callback_received_ = false;
+  struct PendingCallback {
+    std::string name;
+    int64_t seqno = 0;
+    bool ordered;
+  };
+
+  auto AddCallbackInternal(const std::string& name, bool ordered) {
+    auto pb = NewPendingCallback(name, ordered);
+    return [this, pb](auto&&...) { pb->seqno = next_seqno_++; };
+  }
+
+  template <typename Callable>
+  auto AddCallbackInternal(const std::string& name, Callable callback, bool ordered) {
+    auto pb = NewPendingCallback(name, ordered);
+    return [this, pb, callback = std::move(callback)](auto&&... args) {
+      pb->seqno = next_seqno_++;
+      callback(std::forward<decltype(args)>(args)...);
+    };
+  }
+
+  std::pair<std::shared_ptr<ErrorHandler>, fit::function<void(zx_status_t)>> NewErrorHandler(
+      const std::string& name);
+  std::shared_ptr<PendingCallback> NewPendingCallback(const std::string& name, bool ordered);
+
+  void ExpectErrorsInternal(const std::vector<std::shared_ptr<ErrorHandler>>& errors);
+
+  std::unordered_map<zx_handle_t, std::shared_ptr<ErrorHandler>> error_handlers_;
+  std::deque<std::shared_ptr<PendingCallback>> pending_callbacks_;
+  int64_t next_seqno_ = 1;
+  bool new_error_ = false;
 };
 
+// These must be defined in the header file, because of the auto return type, but
+// also must be defined after AddCallbackInternal.
+inline auto TestFixture::AddCallback(const std::string& name) {
+  return AddCallbackInternal(name, true);
+}
+
+template <typename Callable>
+inline auto TestFixture::AddCallback(const std::string& name, Callable callback) {
+  return AddCallbackInternal(name, callback, true);
+}
+
+inline auto TestFixture::AddCallbackUnordered(const std::string& name) {
+  return AddCallbackInternal(name, false);
+}
+
+template <typename Callable>
+inline auto TestFixture::AddCallbackUnordered(const std::string& name, Callable callback) {
+  return AddCallbackInternal(name, callback, false);
+}
+
 }  // namespace media::audio::test
 
 #endif  // SRC_MEDIA_AUDIO_LIB_TEST_TEST_FIXTURE_H_
diff --git a/src/media/audio/lib/test/virtual_device.cc b/src/media/audio/lib/test/virtual_device.cc
index 0afa900..85c807e 100644
--- a/src/media/audio/lib/test/virtual_device.cc
+++ b/src/media/audio/lib/test/virtual_device.cc
@@ -25,7 +25,7 @@
       inspect_id_(inspect_id),
       rb_(format, frame_count) {
   environment->ConnectToService(device_.NewRequest());
-  device_.set_error_handler(fixture->ErrorHandler());
+  fixture->AddErrorHandler(device_, "VirtualAudioDevice");
   WatchEvents();
 
   std::array<uint8_t, 16> device_id_array;