diff --git a/tests/fixtures/project_staged_validation/new_project_added/active/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/new_project_added/active.installed.json
similarity index 74%
rename from tests/fixtures/project_staged_validation/new_project_added/active/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/new_project_added/active.installed.json
index df420decb6b9afd2f34b48172f32c736e2cec1f6..56d81ccad175d109123acfc364f7240c77a897ab 100644
--- a/tests/fixtures/project_staged_validation/new_project_added/active/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/new_project_added/active.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in the active directory of a virtual project.",
+    "It will be compared against staged.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/new_project_added/active/composer.json b/tests/fixtures/project_staged_validation/new_project_added/active/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/new_project_added/active/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/new_project_added/staged/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/new_project_added/staged.installed.json
similarity index 80%
rename from tests/fixtures/project_staged_validation/new_project_added/staged/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/new_project_added/staged.installed.json
index db60c8425c0a5a33340c7436d494b62e3c2a7059..b0c0c6d63ef6dd498e15898fbdbabb92a9ee89c0 100644
--- a/tests/fixtures/project_staged_validation/new_project_added/staged/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/new_project_added/staged.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in a virtual staging area.",
+    "It will be compared against active.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json b/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/active/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/no_errors/active.installed.json
similarity index 81%
rename from tests/fixtures/project_staged_validation/no_errors/active/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/no_errors/active.installed.json
index cc5be9cb71d644d6cf9e7735f865a287a4853c59..65a8de94c62077e2e1d222508e1ba496bf21985a 100644
--- a/tests/fixtures/project_staged_validation/no_errors/active/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/no_errors/active.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in the active directory of a virtual project.",
+    "It will be compared against staged.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/no_errors/active/composer.json b/tests/fixtures/project_staged_validation/no_errors/active/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/no_errors/active/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/composer.json b/tests/fixtures/project_staged_validation/no_errors/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/no_errors/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/staged/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/no_errors/staged.installed.json
similarity index 82%
rename from tests/fixtures/project_staged_validation/no_errors/staged/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/no_errors/staged.installed.json
index e7061278bd8b42cd337d4457ea5d612f690ef23b..f3955584890fdd369df5f31f3c053ed933f957b1 100644
--- a/tests/fixtures/project_staged_validation/no_errors/staged/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/no_errors/staged.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in a virtual staging area.",
+    "It will be compared against active.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/no_errors/staged/composer.json b/tests/fixtures/project_staged_validation/no_errors/staged/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/no_errors/staged/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/project_removed/active/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/project_removed/active.installed.json
similarity index 78%
rename from tests/fixtures/project_staged_validation/project_removed/active/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/project_removed/active.installed.json
index ecede582dfe97f47dc980d770cc023715eb8c966..4ec3e126c98e65c244581bef9342073c7c112bad 100644
--- a/tests/fixtures/project_staged_validation/project_removed/active/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/project_removed/active.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in the active directory of a virtual project.",
+    "It will be compared against staged.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/project_removed/active/composer.json b/tests/fixtures/project_staged_validation/project_removed/active/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/project_removed/active/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/project_removed/staged/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/project_removed/staged.installed.json
similarity index 59%
rename from tests/fixtures/project_staged_validation/project_removed/staged/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/project_removed/staged.installed.json
index d9247b6a62bd0a285f787cbc06aa5b2728e5fa7d..100c033c343aa2a5f9ee23433fe14f14249f3cd1 100644
--- a/tests/fixtures/project_staged_validation/project_removed/staged/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/project_removed/staged.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in a virtual staging area.",
+    "It will be compared against active.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/project_removed/staged/composer.json b/tests/fixtures/project_staged_validation/project_removed/staged/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/project_removed/staged/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/version_changed/active/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/version_changed/active.installed.json
similarity index 74%
rename from tests/fixtures/project_staged_validation/version_changed/active/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/version_changed/active.installed.json
index da5feae3bc3e1721a115b0d927596b6cad798a4c..e7bb050139058310bfaaa0e0966bfc3f82b2eff7 100644
--- a/tests/fixtures/project_staged_validation/version_changed/active/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/version_changed/active.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in the active directory of a virtual project.",
+    "It will be compared against staged.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/version_changed/active/composer.json b/tests/fixtures/project_staged_validation/version_changed/active/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/version_changed/active/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/fixtures/project_staged_validation/version_changed/staged/vendor/composer/installed.json b/tests/fixtures/project_staged_validation/version_changed/staged.installed.json
similarity index 75%
rename from tests/fixtures/project_staged_validation/version_changed/staged/vendor/composer/installed.json
rename to tests/fixtures/project_staged_validation/version_changed/staged.installed.json
index d35464c3cd1b74a3415fd99045e4850e7bd4b178..62a4b1dc724a18e77b05a36fec5a84b3366edf57 100644
--- a/tests/fixtures/project_staged_validation/version_changed/staged/vendor/composer/installed.json
+++ b/tests/fixtures/project_staged_validation/version_changed/staged.installed.json
@@ -1,4 +1,9 @@
 {
+  "_readme": [
+    "This file simuluates a list of packages installed in a virtual staging area.",
+    "It will be compared against active.installed.json.",
+    "See \\Drupal\\Tests\\automatic_updates\\Kernel\\ReadinessValidation\\StagedProjectsValidatorTest::testErrors()"
+  ],
   "packages": [
     {
       "name": "drupal/core",
diff --git a/tests/fixtures/project_staged_validation/version_changed/staged/composer.json b/tests/fixtures/project_staged_validation/version_changed/staged/composer.json
deleted file mode 100644
index 0967ef424bce6791893e9a57bb952f80fd536e93..0000000000000000000000000000000000000000
--- a/tests/fixtures/project_staged_validation/version_changed/staged/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
index 1ea2231aa6500739436726e2a95d2912b62ffd87..8ff6271c6f2b75025753d6c3b8a5d9bccc4dc2b5 100644
--- a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
@@ -4,9 +4,8 @@ namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
 
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-use org\bovigo\vfs\vfsStream;
-use Symfony\Component\Filesystem\Filesystem;
 
 /**
  * @covers \Drupal\automatic_updates\Validator\StagedProjectsValidator
@@ -20,131 +19,100 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
    */
   protected static $modules = ['automatic_updates'];
 
+  /**
+   * The active directory in the virtual file system.
+   *
+   * @var string
+   */
+  private $activeDir;
+
   /**
    * {@inheritdoc}
    */
   protected function setUp(): void {
-    // This test deals with fake sites that don't necessarily have lock files,
-    // so disable lock file validation.
-    $this->disableValidators[] = 'package_manager.validator.lock_file';
     parent::setUp();
+
+    $this->createTestProject();
+    $this->activeDir = $this->container->get('package_manager.path_locator')
+      ->getProjectRoot();
   }
 
   /**
-   * Runs the validator under test against an arbitrary pair of directories.
-   *
-   * @param string $active_dir
-   *   The active directory to validate.
-   * @param string $stage_dir
-   *   The stage directory to validate.
+   * Asserts a set of validation results when staged changes are applied.
    *
-   * @return \Drupal\package_manager\ValidationResult[]
-   *   The validation results.
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
    */
-  private function validate(string $active_dir, string $stage_dir): array {
-    $this->mockPathLocator($active_dir, $active_dir);
-
-    $stage_dir_exists = is_dir($stage_dir);
-    if ($stage_dir_exists) {
-      // If we are testing a fixture with existing stage directory then we
-      // need to use a virtual file system directory, so we can create a
-      // subdirectory using the stage ID after it is created below.
-      $stage_vfs_dir = vfsStream::newDirectory('au_stage');
-      $this->vfsRoot->addChild($stage_vfs_dir);
-      static::$testStagingRoot = $stage_vfs_dir->url();
-    }
-    else {
-      // If we are testing non-existent staging directory we can use the path
-      // directly.
-      static::$testStagingRoot = $stage_dir;
-    }
-
+  private function validate(array $expected_results): void {
+    /** @var \Drupal\automatic_updates\Updater $updater */
     $updater = $this->container->get('automatic_updates.updater');
-    $stage_id = $updater->begin(['drupal' => '9.8.2']);
-    if ($stage_dir_exists) {
-      // Copy the fixture's staging directory into a subdirectory using the
-      // stage ID as the directory name.
-      $sub_directory = vfsStream::newDirectory($stage_id);
-      $stage_vfs_dir->addChild($sub_directory);
-      (new Filesystem())->mirror($stage_dir, $sub_directory->url());
-    }
+    $updater->begin(['drupal' => '9.8.2']);
+    $updater->stage();
 
-    // The staged projects validator only runs before staged updates are
-    // applied. Since the active and stage directories may not exist, we don't
-    // want to invoke the other stages of the update because they may raise
-    // errors that are outside of the scope of what we're testing here.
     try {
       $updater->apply();
-      return [];
+      $this->assertEmpty($expected_results);
     }
     catch (StageValidationException $e) {
-      return $e->getResults();
+      $this->assertValidationResultsEqual($expected_results, $e->getResults());
     }
   }
 
   /**
-   * Tests that if an exception is thrown, the event will absorb it.
+   * Tests that exceptions are turned into validation errors.
    */
   public function testEventConsumesExceptionResults(): void {
-    // Prepare a fake site in the virtual file system which contains valid
-    // Composer data.
-    $fixture = __DIR__ . '/../../../fixtures/fake-site';
-    copy("$fixture/composer.json", 'public://composer.json');
-    mkdir('public://vendor/composer', 0777, TRUE);
-    copy("$fixture/vendor/composer/installed.json", 'public://vendor/composer/installed.json');
-
-    $event_dispatcher = $this->container->get('event_dispatcher');
-    // Disable the disk space validator, since it doesn't work with vfsStream,
-    // and the Git directory excluder, since it won't deal with this tiny
-    // virtual file system correctly.
-    $disable_subscribers = array_map([$this->container, 'get'], [
-      'package_manager.validator.disk_space',
-      'package_manager.git_excluder',
-    ]);
-    array_walk($disable_subscribers, [$event_dispatcher, 'removeSubscriber']);
-
     // Just before the staged changes are applied, delete the composer.json file
     // to trigger an error. This uses the highest possible priority to guarantee
     // it runs before any other subscribers.
-    $listener = function () {
-      unlink('public://composer.json');
+    $listener = function (): void {
+      unlink("$this->activeDir/composer.json");
     };
-    $event_dispatcher->addListener(PreApplyEvent::class, $listener, PHP_INT_MAX);
+    $this->container->get('event_dispatcher')
+      ->addListener(PreApplyEvent::class, $listener, PHP_INT_MAX);
 
-    $results = $this->validate('public://', '/fake/stage/dir');
-    $this->assertCount(1, $results);
-    $messages = reset($results)->getMessages();
-    $this->assertCount(1, $messages);
-    $this->assertStringContainsString('Composer could not find the config file: public:///composer.json', (string) reset($messages));
+    $result = ValidationResult::createError([
+      "Composer could not find the config file: $this->activeDir/composer.json\n",
+    ]);
+    $this->validate([$result]);
   }
 
   /**
-   * Tests validations errors.
+   * Tests validation errors, or lack thereof.
    *
    * @param string $fixtures_dir
-   *   The fixtures directory that provides the active and staged composer.lock
-   *   files.
-   * @param string $expected_summary
-   *   The expected error summary.
+   *   A directory containing `active.installed.json` and
+   *   `staged.installed.json` files. These will be used as the virtual
+   *   project's active and staged `vendor/composer/installed.json` files,
+   *   respectively.
+   * @param string|null $expected_summary
+   *   The expected error summary, or NULL if no errors are expected.
    * @param string[] $expected_messages
-   *   The expected error messages.
+   *   The expected error messages, if any.
    *
    * @dataProvider providerErrors
    */
-  public function testErrors(string $fixtures_dir, string $expected_summary, array $expected_messages): void {
-    $this->assertNotEmpty($fixtures_dir);
-    $this->assertDirectoryExists($fixtures_dir);
-
-    $results = $this->validate("$fixtures_dir/active", "$fixtures_dir/staged");
-    $this->assertCount(1, $results);
-    $result = array_pop($results);
-    $this->assertSame($expected_summary, (string) $result->getSummary());
-    $actual_messages = $result->getMessages();
-    $this->assertCount(count($expected_messages), $actual_messages);
-    foreach ($expected_messages as $message) {
-      $actual_message = array_shift($actual_messages);
-      $this->assertSame($message, (string) $actual_message);
+  public function testErrors(string $fixtures_dir, ?string $expected_summary, array $expected_messages): void {
+    $this->assertFileIsReadable("$fixtures_dir/active.installed.json");
+    $this->assertFileIsReadable("$fixtures_dir/staged.installed.json");
+
+    copy("$fixtures_dir/active.installed.json", "$this->activeDir/vendor/composer/installed.json");
+
+    // Before any other pre-apply listener runs, replaced the staged
+    // `vendor/composer/installed.json` with the fixture's
+    // `staged.installed.json`.
+    $listener = function (PreApplyEvent $event) use ($fixtures_dir): void {
+      copy("$fixtures_dir/staged.installed.json", $event->getStage()->getStageDirectory() . "/vendor/composer/installed.json");
+    };
+    $this->container->get('event_dispatcher')
+      ->addListener(PreApplyEvent::class, $listener, PHP_INT_MAX);
+
+    $expected_results = [];
+    if ($expected_messages) {
+      // @codingStandardsIgnoreLine
+      $expected_results[] = ValidationResult::createError($expected_messages, t($expected_summary));
     }
+    $this->validate($expected_results);
   }
 
   /**
@@ -154,7 +122,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
    *   Test cases for testErrors().
    */
   public function providerErrors(): array {
-    $fixtures_folder = realpath(__DIR__ . '/../../../fixtures/project_staged_validation');
+    $fixtures_folder = __DIR__ . '/../../../fixtures/project_staged_validation';
     return [
       'new_project_added' => [
         "$fixtures_folder/new_project_added",
@@ -180,17 +148,12 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
           "module 'drupal/dev-test_module' from 1.3.0 to 1.3.1.",
         ],
       ],
+      'no_errors' => [
+        "$fixtures_folder/no_errors",
+        NULL,
+        [],
+      ],
     ];
   }
 
-  /**
-   * Tests validation when there are no errors.
-   */
-  public function testNoErrors(): void {
-    $fixtures_dir = realpath(__DIR__ . '/../../../fixtures/project_staged_validation/no_errors');
-    $results = $this->validate("$fixtures_dir/active", "$fixtures_dir/staged");
-    $this->assertIsArray($results);
-    $this->assertEmpty($results);
-  }
-
 }