diff --git a/package_manager/tests/src/Functional/ExcludedPathsTest.php b/package_manager/tests/src/Functional/ExcludedPathsTest.php
deleted file mode 100644
index 184c36206eea67165596b009bba1a8873c6bb44c..0000000000000000000000000000000000000000
--- a/package_manager/tests/src/Functional/ExcludedPathsTest.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-
-namespace Drupal\Tests\package_manager\Functional;
-
-use Drupal\Core\Database\Connection;
-use Drupal\Core\Site\Settings;
-use Drupal\package_manager\PathLocator;
-use Drupal\package_manager\Stage;
-use Drupal\Tests\BrowserTestBase;
-
-/**
- * @covers \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
- *
- * @group package_manager
- */
-class ExcludedPathsTest extends BrowserTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $defaultTheme = 'stark';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'package_manager',
-    'package_manager_bypass',
-  ];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function prepareSettings() {
-    parent::prepareSettings();
-
-    // Disable the filesystem permissions validator, since we cannot guarantee
-    // that the current code base will be writable in all testing situations.
-    // We test this validator functionally in Automatic Updates' build tests,
-    // since those do give us control over the filesystem permissions.
-    // @see \Drupal\Tests\automatic_updates\Build\CoreUpdateTest::assertReadOnlyFileSystemError()
-    // @see \Drupal\Tests\package_manager\Kernel\WritableFileSystemValidatorTest
-    $this->writeSettings([
-      'settings' => [
-        'package_manager_bypass_stager' => (object) [
-          'value' => FALSE,
-          'required' => TRUE,
-        ],
-        'package_manager_bypass_validators' => (object) [
-          'value' => ['package_manager.validator.file_system'],
-          'required' => TRUE,
-        ],
-      ],
-    ]);
-  }
-
-  /**
-   * Tests that certain paths are excluded from staging areas.
-   */
-  public function testExcludedPaths(): void {
-    $active_dir = __DIR__ . '/../../fixtures/fake_site';
-
-    $path_locator = $this->prophesize(PathLocator::class);
-    $path_locator->getActiveDirectory()->willReturn($active_dir);
-
-    $site_path = 'sites/example.com';
-
-    // Ensure that we are using directories within the fake site fixture for
-    // public and private files.
-    $settings = Settings::getAll();
-    $settings['file_public_path'] = "$site_path/files";
-    $settings['file_private_path'] = 'private';
-    new Settings($settings);
-
-    /** @var \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber $subscriber */
-    $subscriber = $this->container->get('package_manager.excluded_paths_subscriber');
-    $reflector = new \ReflectionObject($subscriber);
-    $property = $reflector->getProperty('sitePath');
-    $property->setAccessible(TRUE);
-    $property->setValue($subscriber, $site_path);
-
-    // Mock a SQLite database connection to a file in the active directory. The
-    // file should not be staged.
-    $database = $this->prophesize(Connection::class);
-    $database->driver()->willReturn('sqlite');
-    $database->getConnectionOptions()->willReturn([
-      'database' => $site_path . '/db.sqlite',
-    ]);
-    $property = $reflector->getProperty('database');
-    $property->setAccessible(TRUE);
-    $property->setValue($subscriber, $database->reveal());
-
-    $stage = new class(
-      $path_locator->reveal(),
-      $this->container->get('package_manager.beginner'),
-      $this->container->get('package_manager.stager'),
-      $this->container->get('package_manager.committer'),
-      $this->container->get('file_system'),
-      $this->container->get('event_dispatcher'),
-      $this->container->get('tempstore.shared'),
-    ) extends Stage {
-
-      /**
-       * The directory where staging areas will be created.
-       *
-       * @var string
-       */
-      public static $stagingRoot;
-
-      /**
-       * {@inheritdoc}
-       */
-      protected static function getStagingRoot(): string {
-        return static::$stagingRoot;
-      }
-
-    };
-    $stage::$stagingRoot = $this->siteDirectory . '/stage';
-    $stage_dir = $stage::$stagingRoot . DIRECTORY_SEPARATOR . $stage->create();
-    $this->assertDirectoryExists($stage_dir);
-
-    $ignore = [
-      'sites/simpletest',
-      'vendor/.htaccess',
-      'vendor/web.config',
-      "$site_path/files/ignore.txt",
-      'private/ignore.txt',
-      "$site_path/settings.php",
-      "$site_path/settings.local.php",
-      "$site_path/services.yml",
-      // SQLite databases and their support files should always be ignored.
-      "$site_path/db.sqlite",
-      "$site_path/db.sqlite-shm",
-      "$site_path/db.sqlite-wal",
-      // Default site-specific settings files should be ignored.
-      'sites/default/settings.php',
-      'sites/default/settings.local.php',
-      'sites/default/services.yml',
-    ];
-    foreach ($ignore as $path) {
-      $this->assertFileExists("$active_dir/$path");
-      $this->assertFileDoesNotExist("$stage_dir/$path");
-    }
-    // A non-excluded file in the default site directory should be staged.
-    $this->assertFileExists("$stage_dir/sites/default/stage.txt");
-
-    // A new file added to the staging area in an excluded directory, should not
-    // be copied to the active directory.
-    $file = "$stage_dir/sites/default/no-copy.txt";
-    touch($file);
-    $this->assertFileExists($file);
-    $stage->apply();
-    $this->assertFileDoesNotExist("$active_dir/sites/default/no-copy.txt");
-
-    // The ignored files should still be in the active directory.
-    foreach ($ignore as $path) {
-      $this->assertFileExists("$active_dir/$path");
-    }
-  }
-
-}
diff --git a/package_manager/tests/src/Kernel/ExcludedPathsTest.php b/package_manager/tests/src/Kernel/ExcludedPathsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b9a8eadbe36a68e429f99ef60562c5b48319d4e7
--- /dev/null
+++ b/package_manager/tests/src/Kernel/ExcludedPathsTest.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber;
+use Drupal\package_manager\PathLocator;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @covers \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
+ *
+ * @group package_manager
+ */
+class ExcludedPathsTest extends PackageManagerKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // Ensure that any staging directories created by TestStage are created
+    // in the virtual file system.
+    TestStage::$stagingRoot = $this->vfsRoot->url();
+
+    // We need to rebuild the container after setting a private file path, since
+    // the private stream wrapper is only registered if this setting is set.
+    // @see \Drupal\Core\CoreServiceProvider::register()
+    $this->setSetting('file_private_path', 'private');
+    $kernel = $this->container->get('kernel');
+    $kernel->rebuildContainer();
+    $this->container = $kernel->getContainer();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    // Normally, package_manager_bypass will disable all the actual staging
+    // operations. In this case, we want to perform them so that we can be sure
+    // that files are staged as expected.
+    $this->setSetting('package_manager_bypass_stager', FALSE);
+
+    $container->getDefinition('package_manager.excluded_paths_subscriber')
+      ->setClass(TestExcludedPathsSubscriber::class);
+
+    parent::register($container);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function disableValidators(ContainerBuilder $container): void {
+    parent::disableValidators($container);
+
+    // Disable the disk space validator, since it tries to inspect the file
+    // system in ways that vfsStream doesn't support, like calling stat() and
+    // disk_free_space().
+    $container->removeDefinition('package_manager.validator.disk_space');
+
+    // Disable the lock file and Composer settings validators, since in this
+    // test we have an imaginary file system without any Composer files.
+    $container->removeDefinition('package_manager.validator.lock_file');
+  }
+
+  /**
+   * Tests that certain paths are excluded from staging operations.
+   */
+  public function testExcludedPaths(): void {
+    $site = [
+      'composer.json' => '{}',
+      'private' => [
+        'ignore.txt' => 'This file should never be staged.',
+      ],
+      'sites' => [
+        'default' => [
+          'services.yml' => <<<END
+# This file should never be staged.
+must_not_be: 'empty'
+END,
+          'settings.local.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+          'settings.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+          'stage.txt' => 'This file should be staged.',
+        ],
+        'example.com' => [
+          'files' => [
+            'ignore.txt' => 'This file should never be staged.',
+          ],
+          'db.sqlite' => 'This file should never be staged.',
+          'db.sqlite-shm' => 'This file should never be staged.',
+          'db.sqlite-wal' => 'This file should never be staged.',
+          'services.yml' => <<<END
+# This file should never be staged.
+key: "value"
+END,
+          'settings.local.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+          'settings.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+        ],
+        'simpletest' => [
+          'ignore.txt' => 'This file should never be staged.',
+        ],
+      ],
+      'vendor' => [
+        '.htaccess' => '# This file should never be staged.',
+        'web.config' => 'This file should never be staged.',
+      ],
+    ];
+    vfsStream::create(['active' => $site], $this->vfsRoot);
+
+    $active_dir = $this->vfsRoot->getChild('active')->url();
+
+    $path_locator = $this->prophesize(PathLocator::class);
+    $path_locator->getActiveDirectory()->willReturn($active_dir);
+    $path_locator->getProjectRoot()->willReturn($active_dir);
+    $path_locator->getWebRoot()->willReturn('');
+    $path_locator->getVendorDirectory()->willReturn("$active_dir/vendor");
+    $this->container->set('package_manager.path_locator', $path_locator->reveal());
+
+    $site_path = 'sites/example.com';
+    // Ensure that we are using directories within the fake site fixture for
+    // public and private files.
+    $this->setSetting('file_public_path', "$site_path/files");
+
+    /** @var \Drupal\Tests\package_manager\Kernel\TestExcludedPathsSubscriber $subscriber */
+    $subscriber = $this->container->get('package_manager.excluded_paths_subscriber');
+    $subscriber->sitePath = $site_path;
+
+    // Mock a SQLite database connection to a file in the active directory. The
+    // file should not be staged.
+    $database = $this->prophesize(Connection::class);
+    $database->driver()->willReturn('sqlite');
+    $database->getConnectionOptions()->willReturn([
+      'database' => $site_path . '/db.sqlite',
+    ]);
+    $subscriber->database = $database->reveal();
+
+    $stage = $this->createStage();
+    $stage->create();
+    $stage_dir = $stage->getStageDirectory();
+
+    $ignore = [
+      'sites/simpletest',
+      'vendor/.htaccess',
+      'vendor/web.config',
+      "$site_path/files/ignore.txt",
+      'private/ignore.txt',
+      "$site_path/settings.php",
+      "$site_path/settings.local.php",
+      "$site_path/services.yml",
+      // SQLite databases and their support files should always be ignored.
+      "$site_path/db.sqlite",
+      "$site_path/db.sqlite-shm",
+      "$site_path/db.sqlite-wal",
+      // Default site-specific settings files should be ignored.
+      'sites/default/settings.php',
+      'sites/default/settings.local.php',
+      'sites/default/services.yml',
+    ];
+    foreach ($ignore as $path) {
+      $this->assertFileExists("$active_dir/$path");
+      $this->assertFileDoesNotExist("$stage_dir/$path");
+    }
+    // A non-excluded file in the default site directory should be staged.
+    $this->assertFileExists("$stage_dir/sites/default/stage.txt");
+
+    // A new file added to the staging area in an excluded directory, should not
+    // be copied to the active directory.
+    $file = "$stage_dir/sites/default/no-copy.txt";
+    touch($file);
+    $this->assertFileExists($file);
+    $stage->apply();
+    $this->assertFileDoesNotExist("$active_dir/sites/default/no-copy.txt");
+
+    // The ignored files should still be in the active directory.
+    foreach ($ignore as $path) {
+      $this->assertFileExists("$active_dir/$path");
+    }
+  }
+
+}
+
+/**
+ * A test-only implementation of the excluded path event subscriber.
+ */
+class TestExcludedPathsSubscriber extends ExcludedPathsSubscriber {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $sitePath;
+
+  /**
+   * {@inheritdoc}
+   */
+  public $database;
+
+}
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 6a36af152f85dd574aa43086999a65c73ea95f0c..548535b52892244477dbeacce2db838026ef36b9 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -25,6 +25,14 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
     'package_manager_bypass',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('package_manager');
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -118,6 +126,20 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
  */
 class TestStage extends Stage {
 
+  /**
+   * The directory where staging areas will be created.
+   *
+   * @var string
+   */
+  public static $stagingRoot;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getStagingRoot(): string {
+    return static::$stagingRoot ?: parent::getStagingRoot();
+  }
+
   /**
    * {@inheritdoc}
    */