diff --git a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..b55aed5bbaa00508c29b07ae8d680956e04d5822
--- /dev/null
+++ b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Base class for functional tests of the Automatic Updates module.
+ */
+abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['update', 'update_test'];
+
+  /**
+   * Sets the current (running) version of core, as known to the Update module.
+   *
+   * @param string $version
+   *   The current version of core.
+   */
+  protected function setCoreVersion(string $version): void {
+    $this->config('update_test.settings')
+      ->set('system_info.#all.version', $version)
+      ->save();
+  }
+
+  /**
+   * Sets the release metadata file to use when fetching available updates.
+   *
+   * @param string $file
+   *   The path of the XML metadata file to use.
+   */
+  protected function setReleaseMetadata(string $file): void {
+    $this->config('update.settings')
+      ->set('fetch.url', $this->baseUrl . '/automatic-update-test')
+      ->save();
+
+    [$project, $fixture] = explode('.', basename($file, '.xml'), 2);
+    $this->config('update_test.settings')
+      ->set('xml_map', [
+        $project => $fixture,
+      ])
+      ->save();
+  }
+
+}
diff --git a/tests/src/Functional/FileSystemOperationsTest.php b/tests/src/Functional/FileSystemOperationsTest.php
index 0935b37aef161b5ae34999b11a9de2e6d2df9d02..1347ceb3b408399fc6703dc2932e8b22db9421ef 100644
--- a/tests/src/Functional/FileSystemOperationsTest.php
+++ b/tests/src/Functional/FileSystemOperationsTest.php
@@ -6,19 +6,18 @@ use Drupal\automatic_updates\ComposerStager\Cleaner;
 use Drupal\automatic_updates\PathLocator;
 use Drupal\automatic_updates\Updater;
 use Drupal\Core\Site\Settings;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests handling of files and directories during an update.
  *
  * @group automatic_updates
  */
-class FileSystemOperationsTest extends BrowserTestBase {
+class FileSystemOperationsTest extends AutomaticUpdatesFunctionalTestBase {
 
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['automatic_updates_test', 'update_test'];
+  protected static $modules = ['automatic_updates_test'];
 
   /**
    * {@inheritdoc}
@@ -91,17 +90,8 @@ class FileSystemOperationsTest extends BrowserTestBase {
     // \Drupal\automatic_updates\Validator\UpdateVersionValidator, that need to
     // fetch release metadata. We need to ensure that those HTTP request(s)
     // succeed, so set them up to point to our fake release metadata.
-    $this->config('update_test.settings')
-      ->set('xml_map', [
-        'drupal' => '0.0',
-      ])
-      ->save();
-    $this->config('update.settings')
-      ->set('fetch.url', $this->baseUrl . '/automatic-update-test')
-      ->save();
-    $this->config('update_test.settings')
-      ->set('system_info.#all.version', '9.8.0')
-      ->save();
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml');
+    $this->setCoreVersion('9.8.0');
   }
 
   /**
diff --git a/tests/src/Functional/UpdaterFormTest.php b/tests/src/Functional/UpdaterFormTest.php
index d03229896d367e8167e963a042b64843fa966206..dd5422aaf652cc9bd2d088e28b9c08107a008f3e 100644
--- a/tests/src/Functional/UpdaterFormTest.php
+++ b/tests/src/Functional/UpdaterFormTest.php
@@ -7,14 +7,13 @@ use Drupal\automatic_updates\Exception\UpdateException;
 use Drupal\automatic_updates\Validation\ValidationResult;
 use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1;
 use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * @covers \Drupal\automatic_updates\Form\UpdaterForm
  *
  * @group automatic_updates
  */
-class UpdaterFormTest extends BrowserTestBase {
+class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
 
   use ValidationTestTrait;
 
@@ -31,36 +30,14 @@ class UpdaterFormTest extends BrowserTestBase {
     'automatic_updates',
     'automatic_updates_test',
     'package_manager_bypass',
-    'update_test',
   ];
 
-  /**
-   * Sets the running version of core, as known to the Update module.
-   *
-   * @param string $version
-   *   The version of core to set. When checking for updates, this is what the
-   *   Update module will think the running version of core is.
-   */
-  private function setCoreVersion(string $version): void {
-    $this->config('update_test.settings')
-      ->set('system_info.#all.version', $version)
-      ->save();
-  }
-
   /**
    * {@inheritdoc}
    */
   protected function setUp(): void {
     parent::setUp();
-
-    $this->config('update_test.settings')
-      ->set('xml_map', [
-        'drupal' => '0.0',
-      ])
-      ->save();
-    $this->config('update.settings')
-      ->set('fetch.url', $this->baseUrl . '/automatic-update-test')
-      ->save();
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml');
   }
 
   /**
diff --git a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..47ab4b3eedb4b5df15b83df6fb34385e9e05092f
--- /dev/null
+++ b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use GuzzleHttp\Client;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Utils;
+
+/**
+ * Base class for kernel tests of the Automatic Updates module.
+ */
+abstract class AutomaticUpdatesKernelTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['update', 'update_test'];
+
+  /**
+   * Sets the current (running) version of core, as known to the Update module.
+   *
+   * @param string $version
+   *   The current version of core.
+   */
+  protected function setCoreVersion(string $version): void {
+    $this->config('update_test.settings')
+      ->set('system_info.#all.version', $version)
+      ->save();
+  }
+
+  /**
+   * Sets the release metadata file to use when fetching available updates.
+   *
+   * @param string $file
+   *   The path of the XML metadata file to use.
+   */
+  protected function setReleaseMetadata(string $file): void {
+    $metadata = Utils::tryFopen($file, 'r');
+    $response = new Response(200, [], Utils::streamFor($metadata));
+    $handler = new MockHandler([$response]);
+    $client = new Client([
+      'handler' => HandlerStack::create($handler),
+    ]);
+    $this->container->set('http_client', $client);
+  }
+
+}
diff --git a/tests/src/Kernel/UpdateRecommenderTest.php b/tests/src/Kernel/UpdateRecommenderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8de05d44d8efa90956e7dfc3d92cfa9ece80070e
--- /dev/null
+++ b/tests/src/Kernel/UpdateRecommenderTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Kernel;
+
+use Drupal\automatic_updates\UpdateRecommender;
+
+/**
+ * @covers \Drupal\automatic_updates\UpdateRecommender
+ *
+ * @group automatic_updates
+ */
+class UpdateRecommenderTest extends AutomaticUpdatesKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'automatic_updates',
+    'package_manager',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('update');
+  }
+
+  /**
+   * Tests fetching the recommended release when an update is available.
+   */
+  public function testUpdateAvailable(): void {
+    $this->setCoreVersion('9.8.0');
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml');
+
+    $recommender = new UpdateRecommender();
+    $recommended_release = $recommender->getRecommendedRelease(TRUE);
+    $this->assertNotEmpty($recommended_release);
+    $this->assertSame('9.8.1', $recommended_release->getVersion());
+    // Getting the recommended release again should not trigger another request.
+    $this->assertNotEmpty($recommender->getRecommendedRelease());
+  }
+
+  /**
+   * Tests fetching the recommended release when there is no update available.
+   */
+  public function testNoUpdateAvailable(): void {
+    $this->setCoreVersion('9.8.1');
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml');
+
+    $recommender = new UpdateRecommender();
+    $recommended_release = $recommender->getRecommendedRelease(TRUE);
+    $this->assertNull($recommended_release);
+    // Getting the recommended release again should not trigger another request.
+    $this->assertNull($recommender->getRecommendedRelease());
+  }
+
+}
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 7b0080ffe5457fb3ea870a2d1203cbe7f78e7c0b..2151697e8e1f7039f122682505805cd4d5538194 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -2,11 +2,6 @@
 
 namespace Drupal\Tests\automatic_updates\Kernel;
 
-use Drupal\KernelTests\KernelTestBase;
-use GuzzleHttp\Client;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use GuzzleHttp\Psr7\Utils;
 use Prophecy\Argument;
 
 /**
@@ -14,7 +9,7 @@ use Prophecy\Argument;
  *
  * @group automatic_updates
  */
-class UpdaterTest extends KernelTestBase {
+class UpdaterTest extends AutomaticUpdatesKernelTestBase {
 
   /**
    * {@inheritdoc}
@@ -23,25 +18,16 @@ class UpdaterTest extends KernelTestBase {
     'automatic_updates',
     'automatic_updates_test',
     'package_manager',
-    'update',
-    'update_test',
   ];
 
   /**
    * Tests that correct versions are staged after calling ::begin().
    */
   public function testCorrectVersionsStaged() {
-    // Ensure that the HTTP client will fetch our fake release metadata.
-    $release_data = Utils::tryFopen(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml', 'r');
-    $response = new Response(200, [], Utils::streamFor($release_data));
-    $handler = new MockHandler([$response]);
-    $client = new Client(['handler' => $handler]);
-    $this->container->set('http_client', $client);
+    $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.0.0.xml');
 
     // Set the running core version to 9.8.0.
-    $this->config('update_test.settings')
-      ->set('system_info.#all.version', '9.8.0')
-      ->save();
+    $this->setCoreVersion('9.8.0');
 
     $this->container->get('automatic_updates.updater')->begin([
       'drupal' => '9.8.1',