diff --git a/package_manager/src/Validator/ComposerExecutableValidator.php b/package_manager/src/Validator/ComposerExecutableValidator.php
index 280883ce133c18034e436da601e9c4d46389fc4e..959703f55b2728c940438c1894828e0a9115bae9 100644
--- a/package_manager/src/Validator/ComposerExecutableValidator.php
+++ b/package_manager/src/Validator/ComposerExecutableValidator.php
@@ -23,7 +23,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  *   at any time without warning. External code should not interact with this
  *   class.
  */
-final class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOutputCallbackInterface {
+class ComposerExecutableValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
@@ -48,13 +48,6 @@ final class ComposerExecutableValidator implements EventSubscriberInterface, Pro
    */
   protected $moduleHandler;
 
-  /**
-   * The detected version of Composer.
-   *
-   * @var string
-   */
-  protected $version;
-
   /**
    * Constructs a ComposerExecutableValidator object.
    *
@@ -76,18 +69,24 @@ final class ComposerExecutableValidator implements EventSubscriberInterface, Pro
    */
   public function validateStagePreOperation(PreOperationStageEvent $event): void {
     try {
-      $this->composer->run(['--version'], $this);
+      $output = $this->runCommand();
     }
     catch (ExceptionInterface $e) {
       $this->setError($e->getMessage(), $event);
       return;
     }
 
-    if ($this->version) {
-      if (!Semver::satisfies($this->version, static::MINIMUM_COMPOSER_VERSION_CONSTRAINT)) {
+    $matched = [];
+    // Search for a semantic version number and optional stability flag.
+    if (preg_match('/([0-9]+\.?){3}-?((alpha|beta|rc)[0-9]*)?/i', $output, $matched)) {
+      $version = $matched[0];
+    }
+
+    if (isset($version)) {
+      if (!Semver::satisfies($version, static::MINIMUM_COMPOSER_VERSION_CONSTRAINT)) {
         $message = $this->t('A Composer version which satisfies <code>@minimum_version</code> is required, but version @detected_version was detected.', [
           '@minimum_version' => static::MINIMUM_COMPOSER_VERSION_CONSTRAINT,
-          '@detected_version' => $this->version,
+          '@detected_version' => $version,
         ]);
         $this->setError($message, $event);
       }
@@ -133,14 +132,35 @@ final class ComposerExecutableValidator implements EventSubscriberInterface, Pro
   }
 
   /**
-   * {@inheritdoc}
+   * Runs `composer --version` and returns its output.
+   *
+   * @return string
+   *   The output of `composer --version`.
    */
-  public function __invoke(string $type, string $buffer): void {
-    $matched = [];
-    // Search for a semantic version number and optional stability flag.
-    if (preg_match('/([0-9]+\.?){3}-?((alpha|beta|rc)[0-9]*)?/i', $buffer, $matched)) {
-      $this->version = $matched[0];
-    }
+  protected function runCommand(): string {
+    // For whatever reason, PHPCS thinks that $output is not used, even though
+    // it very clearly *is*. So, shut PHPCS up for the duration of this method.
+    // phpcs:disable
+    $callback = new class () implements ProcessOutputCallbackInterface {
+
+      /**
+       * The command output.
+       *
+       * @var string
+       */
+      public string $output = '';
+
+      /**
+       * {@inheritdoc}
+       */
+      public function __invoke(string $type, string $buffer): void {
+        $this->output .= $buffer;
+      }
+
+    };
+    $this->composer->run(['--version'], $callback);
+    return $callback->output;
+    // phpcs:enable
   }
 
 }
diff --git a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
index 07dd32553bc149ae7e334f8f43f8e3cca7bfaf51..bea65d091bf8aad3ac41830d2c3bc199f4699f52 100644
--- a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
@@ -8,8 +8,7 @@ use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Validator\ComposerExecutableValidator;
 use Drupal\package_manager\ValidationResult;
 use PhpTuf\ComposerStager\Domain\Exception\IOException;
-use PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ComposerRunnerInterface;
-use Prophecy\Argument;
+use PHPUnit\Framework\Assert;
 
 /**
  * @covers \Drupal\package_manager\Validator\ComposerExecutableValidator
@@ -18,21 +17,6 @@ use Prophecy\Argument;
  */
 class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
 
-  /**
-   * The mocked Composer runner.
-   *
-   * @var \Prophecy\Prophecy\ObjectProphecy|\PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ComposerRunnerInterface
-   */
-  private $composerRunner;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp(): void {
-    $this->composerRunner = $this->prophesize(ComposerRunnerInterface::class);
-    parent::setUp();
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -40,7 +24,7 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
     parent::register($container);
 
     $container->getDefinition('package_manager.validator.composer_executable')
-      ->setArgument('$composer', $this->composerRunner->reveal());
+      ->setClass(TestComposerExecutableValidator::class);
   }
 
   /**
@@ -48,12 +32,7 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
    */
   public function testErrorIfComposerNotFound(): void {
     $exception = new IOException("This is your regularly scheduled error.");
-
-    // If the Composer executable isn't found, the executable finder will throw
-    // an exception, which will not be caught by the Composer runner.
-    $this->composerRunner->run(Argument::cetera())
-      ->willThrow($exception)
-      ->shouldBeCalled();
+    TestComposerExecutableValidator::setCommandOutput($exception);
 
     // The validator should translate that exception into an error.
     $error = ValidationResult::createError([
@@ -163,21 +142,7 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerComposerVersionValidation
    */
   public function testComposerVersionValidation(string $reported_version, array $expected_results): void {
-    // Mock the output of `composer --version`, will be passed to the validator,
-    // which is itself a callback function that gets called repeatedly as
-    // Composer produces output.
-    $this->composerRunner->run(['--version'], Argument::type(ComposerExecutableValidator::class))
-      // Whatever is passed to ::run() will be passed to this mock callback in
-      // $arguments, and we know exactly what that will contain: an array of
-      // command arguments for Composer, and the validator object.
-      ->will(function (array $arguments) use ($reported_version) {
-        /** @var \Drupal\package_manager\Validator\ComposerExecutableValidator $validator */
-        $validator = $arguments[1];
-        // Invoke the validator (which, as mentioned, is a callback function),
-        // with fake output from `composer --version`. It should try to tease a
-        // recognized, supported version number out of this output.
-        $validator($validator::OUT, "Composer version $reported_version");
-      });
+    TestComposerExecutableValidator::setCommandOutput("Composer version $reported_version");
 
     // If the validator can't find a recognized, supported version of Composer,
     // it should produce errors.
@@ -220,3 +185,32 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
   }
 
 }
+
+/**
+ * A test-only version of ComposerExecutableValidator that returns set output.
+ */
+class TestComposerExecutableValidator extends ComposerExecutableValidator {
+
+  /**
+   * Sets the output of `composer --version`.
+   *
+   * @param string|\Throwable $output
+   *   The output of the command, or an exception to throw.
+   */
+  public static function setCommandOutput($output): void {
+    \Drupal::state()->set(static::class, $output);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function runCommand(): string {
+    $output = \Drupal::state()->get(static::class);
+    Assert::assertNotNull($output, __CLASS__ . '::setCommandOutput() should have been called first 💩');
+    if ($output instanceof \Throwable) {
+      throw $output;
+    }
+    return $output;
+  }
+
+}