diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
new file mode 100644
index 0000000..500fd41
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
@@ -0,0 +1,359 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\UpdatePathTestBase.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Database\Database;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\MissingDependencyException;
+use Drupal\Core\Session\UserSession;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Url;
+use Drupal\simpletest\WebTestBase;
+use Drupal\user\Entity\User;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * Provides a base class that loads a database as a starting point.
+ */
+abstract class UpdatePathTestBase extends WebTestBase {
+
+  /**
+   * Modules to enable after the database is loaded.
+   */
+  protected static $modules = [];
+
+ /**
+   * The file path(s) to the dumped database(s) to load into the child site.
+   *
+   * @var array
+   */
+  protected $databaseDumpFiles = [];
+
+  /**
+   * The install profile used in the database dump file.
+   *
+   * @var string
+   */
+  protected $installProfile = 'standard';
+
+  /**
+   * Flag that indicates whether the child site has been upgraded.
+   *
+   * @var bool
+   */
+  protected $upgradedSite = FALSE;
+
+  /**
+   * Array of errors triggered during the upgrade process.
+   *
+   * @var array
+   */
+  protected $upgradeErrors = [];
+
+  /**
+   * Array of modules loaded when the test starts.
+   *
+   * @var array
+   */
+  protected $loadedModules = [];
+
+  /**
+   * Flag to indicate whether zlib is installed or not.
+   *
+   * @var bool
+   */
+  protected $zlibInstalled = TRUE;
+
+  /**
+   * Flag to indicate whether there are pending updates or not.
+   *
+   * @var bool
+   */
+  protected $pendingUpdates = TRUE;
+
+  /**
+   * The update URL.
+   *
+   * @var string
+   */
+  protected $updateUrl;
+
+  /**
+   * Constructs an UpdatePathTestCase object.
+   *
+   * @param $test_id
+   *   (optional) The ID of the test. Tests with the same id are reported
+   *   together.
+   */
+  function __construct($test_id = NULL) {
+    parent::__construct($test_id);
+    $this->zlibInstalled = function_exists('gzopen');
+  }
+
+  /**
+   * Overrides WebTestBase::setUp() for upgrade testing.
+   *
+   * @see \Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
+   * @see \Drupal\simpletest\WebTestBase::changeDatabasePrefix()
+   * @see \Drupal\simpletest\WebTestBase::prepareEnvironment()
+   */
+  protected function setUp() {
+    // We are going to set a missing zlib requirement property for usage
+    // during the performUpgrade() and tearDown() methods. Also set that the
+    // tests failed.
+    if (!$this->zlibInstalled) {
+      parent::setUp();
+      return;
+    }
+
+    // Set the update url.
+    $this->updateUrl = Url::fromRoute('system.db_update');
+
+    // When running tests through the Simpletest UI (vs. on the command line),
+    // Simpletest's batch conflicts with the installer's batch. Batch API does
+    // not support the concept of nested batches (in which the nested is not
+    // progressive), so we need to temporarily pretend there was no batch.
+    // Backup the currently running Simpletest batch.
+    $this->originalBatch = batch_get();
+
+    // Define information about the user 1 account.
+    $this->rootUser = new UserSession(array(
+      'uid' => 1,
+      'name' => 'admin',
+      'mail' => 'admin@example.com',
+      'pass_raw' => $this->randomMachineName(),
+    ));
+
+    // The child site derives its session name from the database prefix when
+    // running web tests.
+    $this->generateSessionName($this->databasePrefix);
+
+    // Reset the static batch to remove Simpletest's batch operations.
+    $batch = &batch_get();
+    $batch = array();
+
+    // Get parameters for install_drupal() before removing global variables.
+    $parameters = $this->installParameters();
+
+    // Prepare installer settings that are not install_drupal() parameters.
+    // Copy and prepare an actual settings.php, so as to resemble a regular
+    // installation.
+    // Not using File API; a potential error must trigger a PHP warning.
+    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
+    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');
+    copy(DRUPAL_ROOT . '/sites/default/default.services.yml', $directory . '/services.yml');
+
+    // All file system paths are created by System module during installation.
+    // @see system_requirements()
+    // @see TestBase::prepareEnvironment()
+    $settings['settings']['file_public_path'] = (object) array(
+      'value' => $this->publicFilesDirectory,
+      'required' => TRUE,
+    );
+    $settings['settings']['file_private_path'] = (object) array(
+      'value' => $this->privateFilesDirectory,
+      'required' => TRUE,
+    );
+    // Save the original site directory path, so that extensions in the
+    // site-specific directory can still be discovered in the test site
+    // environment.
+    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
+    $settings['settings']['test_parent_site'] = (object) array(
+      'value' => $this->originalSite,
+      'required' => TRUE,
+    );
+    // Add the parent profile's search path to the child site's search paths.
+    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
+    $settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
+      'value' => $this->originalProfile,
+      'required' => TRUE,
+    );
+    $settings['settings']['apcu_ensure_unique_prefix'] = (object) array(
+      'value' => FALSE,
+      'required' => TRUE,
+    );
+    // Remember the profile which was used.
+    $settings['settings']['install_profile'] = (object) array(
+      'value' => $this->installProfile,
+      'required' => TRUE,
+    );
+
+    // Generate a hash salt.
+    // @todo should this be part of the exported database somehow?
+    $settings['settings']['hash_salt'] = (object) array(
+      'value'    => Crypt::randomBytesBase64(55),
+      'required' => TRUE,
+    );
+
+    // Since the installer isn't run, add the database settings here too.
+    $settings['databases']['default'] = (object) array(
+      'value' => Database::getConnectionInfo(),
+      'required' => TRUE,
+    );
+
+    $this->writeSettings($settings);
+    // Allow for test-specific overrides.
+    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
+    if (file_exists($settings_testing_file)) {
+      // Copy the testing-specific settings.php overrides in place.
+      copy($settings_testing_file, $directory . '/settings.testing.php');
+      // Add the name of the testing class to settings.php and include the
+      // testing specific overrides
+      file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) ."';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' ."\n", FILE_APPEND);
+    }
+    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
+    if (file_exists($settings_services_file)) {
+      // Copy the testing-specific service overrides in place.
+      copy($settings_services_file, $directory . '/services.yml');
+    }
+    if ($this->strictConfigSchema) {
+      // Add a listener to validate configuration schema on save.
+      $yaml = new Yaml();
+      $content = file_get_contents($directory . '/services.yml');
+      $services = $yaml->parse($content);
+      $services['services']['simpletest.config_schema_checker'] = [
+        'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker',
+        'arguments' => ['@config.typed'],
+        'tags' => [['name' => 'event_subscriber']]
+      ];
+      file_put_contents($directory . '/services.yml', $yaml->dump($services));
+    }
+    // Since Drupal is bootstrapped already, install_begin_request() will not
+    // bootstrap again. Hence, we have to reload the newly written custom
+    // settings.php manually.
+    $class_loader = require DRUPAL_ROOT . '/autoload.php';
+    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader);
+
+    // Load the database(s).
+    foreach ($this->databaseDumpFiles as $file) {
+      if (substr($file, -3) == '.gz') {
+        $file = "compress.zlib://$file";
+      }
+      require $file;
+    }
+
+    // Import new settings.php.
+    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader);
+
+    // After writing settings.php, the installer removes write permissions
+    // from the site directory. To allow drupal_generate_test_ua() to write
+    // a file containing the private key for drupal_valid_test_ua(), the site
+    // directory has to be writable.
+    // TestBase::restoreEnvironment() will delete the entire site directory.
+    // Not using File API; a potential error must trigger a PHP warning.
+    chmod($directory, 0777);
+
+    $request = Request::createFromGlobals();
+    $this->kernel = DrupalKernel::createFromRequest($request, $class_loader, 'prod', TRUE);
+    $this->kernel->prepareLegacyRequest($request);
+    // Force the container to be built from scratch instead of loaded from the
+    // disk. This forces us to not accidentally load the parent site.
+    $container = $this->kernel->rebuildContainer();
+
+    // Add the config directories to settings.php.
+    drupal_install_config_directories();
+
+    $config = $container->get('config.factory');
+
+    // Manually create and configure private and temporary files directories.
+    // While these could be preset/enforced in settings.php like the public
+    // files directory above, some tests expect them to be configurable in the
+    // UI. If declared in settings.php, they would no longer be configurable.
+    file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
+    file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
+    $config->getEditable('system.file')
+      ->set('path.temporary', $this->tempFilesDirectory)
+      ->save();
+
+    // Manually configure the test mail collector implementation to prevent
+    // tests from sending out emails and collect them in state instead.
+    // While this should be enforced via settings.php prior to installation,
+    // some tests expect to be able to test mail system implementations.
+    $config->getEditable('system.mail')
+      ->set('interface.default', 'test_mail_collector')
+      ->save();
+
+    // By default, verbosely display all errors and disable all production
+    // environment optimizations for all tests to avoid needless overhead and
+    // ensure a sane default experience for test authors.
+    // @see https://www.drupal.org/node/2259167
+    $config->getEditable('system.logging')
+      ->set('error_level', 'verbose')
+      ->save();
+    $config->getEditable('system.performance')
+      ->set('css.preprocess', FALSE)
+      ->set('js.preprocess', FALSE)
+      ->save();
+
+    // Collect modules to install.
+    $class = get_class($this);
+    $modules = array();
+    while ($class) {
+      if (property_exists($class, 'modules')) {
+        $modules = array_merge($modules, $class::$modules);
+      }
+      $class = get_parent_class($class);
+    }
+    if ($modules) {
+      $modules = array_unique($modules);
+      try {
+        $success = $container->get('module_installer')->install($modules, TRUE);
+        $this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
+      }
+      catch (MissingDependencyException $e) {
+        // The exception message has all the details.
+        $this->fail($e->getMessage());
+      }
+
+      $this->rebuildContainer();
+    }
+
+    // Restore the original Simpletest batch.
+    $batch = &batch_get();
+    $batch = $this->originalBatch;
+
+    // Reset/rebuild all data structures after enabling the modules, primarily
+    // to synchronize all data structures and caches between the test runner and
+    // the child site.
+    // @see \Drupal\Core\DrupalKernel::bootCode()
+    // @todo Test-specific setUp() methods may set up further fixtures; find a
+    //   way to execute this after setUp() is done, or to eliminate it entirely.
+    $this->resetAll();
+    $this->kernel->prepareLegacyRequest($request);
+
+    // Explicitly call register() again on the container registered in \Drupal.
+    // @todo This should already be called through
+    //   DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that
+    //   appears to be calling a different container.
+    $this->container->get('stream_wrapper_manager')->register();
+
+    // Replace User 1 with the user created here.
+    /** @var \Drupal\user\UserInterface $account */
+    $account = User::load(1);
+    $account->setPassword($this->rootUser->pass_raw);
+    $account->setEmail($this->rootUser->getEmail());
+    $account->setUsername($this->rootUser->getUsername());
+    $account->save();
+  }
+
+  /**
+   * Helper function to run pending database updates.
+   */
+  protected function runUpdates() {
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->clickLink(t('Continue'));
+
+    // Run the update hooks.
+    $this->clickLink(t('Apply pending updates'));
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php b/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php
new file mode 100644
index 0000000..92a3fb4
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\UpdatePathTestBaseTest.php
+ */
+
+namespace Drupal\system\Tests\Update;
+use Drupal\Component\Utility\SafeMarkup;
+
+/**
+ * Tests the update path base class.
+ *
+ * @group Update
+ */
+class UpdatePathTestBaseTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['update_test_schema'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->databaseDumpFiles = [__DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz'];
+    parent::setUp();
+  }
+
+  /**
+   * Tests that the database was properly loaded.
+   */
+  protected function testDatabaseLoaded() {
+    foreach (['user', 'node', 'system', 'update_test_schema'] as $module) {
+      $this->assertEqual(drupal_get_installed_schema_version($module), 8000, SafeMarkup::format('Module @module schema is 8000', ['@module' => $module]));
+    }
+    $this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install');
+    $this->drupalGet('<front>');
+    $this->assertText('Site-Install');
+  }
+
+  /**
+   * Test that updates are properly run.
+   */
+  protected function testUpdateHookN() {
+    // Increment the schema version.
+    \Drupal::state()->set('update_test_schema_version', 8001);
+    $this->runUpdates();
+    // Ensure schema has changed.
+    $this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
+    // Ensure the index was added for column a.
+    $this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/UpdateSchemaTest.php b/core/modules/system/src/Tests/Update/UpdateSchemaTest.php
new file mode 100644
index 0000000..b7ea05e
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdateSchemaTest.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Contains \Drupal\system\Tests\Update\UpdateSchemaTest.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+use Drupal\Core\Url;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests that update hooks are properly run.
+ *
+ * @group Update
+ */
+class UpdateSchemaTest extends WebTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['update_test_schema'];
+
+  /**
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * The update URL.
+   *
+   * @var string
+   */
+  protected $updateUrl;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    require_once \Drupal::root() . '/core/includes/update.inc';
+    $this->user = $this->drupalCreateUser(['administer software updates', 'access site in maintenance mode']);
+    $this->updateUrl = Url::fromRoute('system.db_update');
+  }
+
+  /**
+   * Tests that update hooks are properly run.
+   */
+  function testUpdateHooks() {
+    // Verify that the 8000 schema is in place.
+    $this->assertEqual(drupal_get_installed_schema_version('update_test_schema'), 8000);
+    $this->assertFalse(db_index_exists('update_test_schema_table', 'test'), 'Version 8000 of the update_test_schema module is installed.');
+
+    // Increment the schema version.
+    \Drupal::state()->set('update_test_schema_version', 8001);
+
+    $this->drupalLogin($this->user);
+    $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->clickLink(t('Continue'));
+    $this->assertRaw('Schema version 8001.');
+    // Run the update hooks.
+    $this->clickLink(t('Apply pending updates'));
+
+    // Ensure schema has changed.
+    $this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
+    // Ensure the index was added for column a.
+    $this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml b/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml
new file mode 100644
index 0000000..620fb92
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml
@@ -0,0 +1,6 @@
+name: 'Update test schema'
+type: module
+description: 'Support module for update testing.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/update_test_schema/update_test_schema.install b/core/modules/system/tests/modules/update_test_schema/update_test_schema.install
new file mode 100644
index 0000000..56ce567
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_schema/update_test_schema.install
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Update hooks and schema definition for the update_test_schema module.
+ */
+
+/**
+ * Implements hook_schema().
+ *
+ * The schema defined here will vary on state to allow for update hook testing.
+ */
+function update_test_schema_schema() {
+  $schema_version = \Drupal::state()->get('update_test_schema_version', 8000);
+  $table = [
+    'fields' => [
+      'a' => ['type' => 'int', 'not null' => TRUE],
+      'b' => ['type' => 'blob', 'not null' => FALSE],
+    ],
+  ];
+  switch ($schema_version) {
+    case 8001:
+      // Add the index.
+      $table['indexes']['test'] = ['a'];
+      break;
+  }
+  return ['update_test_schema_table' => $table];
+}
+
+// Update hooks are defined depending on state as well.
+$next = \Drupal::state()->get('update_test_schema_version', 8000) + 1;
+
+if ($next > 8001) {
+  /**
+   * Schema version 8001.
+   */
+  function update_test_schema_update_8001() {
+    // Add a column.
+    db_add_index('update_test_schema_table', 'test', ['a']);
+  }
+}
