diff --git a/core/lib/Drupal/Core/Test/TestDatabase.php b/core/lib/Drupal/Core/Test/TestDatabase.php
index e126b3aca4903de6a94da46fe5335fb0d5e6b43d..20cc8e66870515b96650bd1735c0bf1e91a0a577 100644
--- a/core/lib/Drupal/Core/Test/TestDatabase.php
+++ b/core/lib/Drupal/Core/Test/TestDatabase.php
@@ -179,4 +179,226 @@ protected function getLockFile($lock_id) {
     return FileSystem::getOsTemporaryDirectory() . '/test_' . $lock_id;
   }
 
+  /**
+   * Store an assertion from outside the testing context.
+   *
+   * This is useful for inserting assertions that can only be recorded after
+   * the test case has been destroyed, such as PHP fatal errors. The caller
+   * information is not automatically gathered since the caller is most likely
+   * inserting the assertion on behalf of other code. In all other respects
+   * the method behaves just like \Drupal\simpletest\TestBase::assert() in terms
+   * of storing the assertion.
+   *
+   * @param string $test_id
+   *   The test ID to which the assertion relates.
+   * @param string $test_class
+   *   The test class to store an assertion for.
+   * @param bool|string $status
+   *   A boolean or a string of 'pass' or 'fail'. TRUE means 'pass'.
+   * @param string $message
+   *   The assertion message.
+   * @param string $group
+   *   The assertion message group.
+   * @param array $caller
+   *   The an array containing the keys 'file' and 'line' that represent the
+   *   file and line number of that file that is responsible for the assertion.
+   *
+   * @return int
+   *   Message ID of the stored assertion.
+   *
+   * @internal
+   */
+  public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = []) {
+    // Convert boolean status to string status.
+    if (is_bool($status)) {
+      $status = $status ? 'pass' : 'fail';
+    }
+
+    $caller += [
+      'function' => 'Unknown',
+      'line' => 0,
+      'file' => 'Unknown',
+    ];
+
+    $assertion = [
+      'test_id' => $test_id,
+      'test_class' => $test_class,
+      'status' => $status,
+      'message' => $message,
+      'message_group' => $group,
+      'function' => $caller['function'],
+      'line' => $caller['line'],
+      'file' => $caller['file'],
+    ];
+
+    return static::getConnection()
+      ->insert('simpletest')
+      ->fields($assertion)
+      ->execute();
+  }
+
+  /**
+   * Get information about the last test that ran given a test ID.
+   *
+   * @param int $test_id
+   *   The test ID to get the last test from.
+   *
+   * @return array
+   *   Array containing the last database prefix used and the last test class
+   *   that ran.
+   *
+   * @internal
+   */
+  public static function lastTestGet($test_id) {
+    $connection = static::getConnection();
+    $last_prefix = $connection
+      ->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, [
+        ':test_id' => $test_id,
+      ])
+      ->fetchField();
+    $last_test_class = $connection
+      ->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, [
+        ':test_id' => $test_id,
+      ])
+      ->fetchField();
+    return [$last_prefix, $last_test_class];
+  }
+
+  /**
+   * Reads the error log and reports any errors as assertion failures.
+   *
+   * The errors in the log should only be fatal errors since any other errors
+   * will have been recorded by the error handler.
+   *
+   * @param int $test_id
+   *   The test ID to which the log relates.
+   * @param string $test_class
+   *   The test class to which the log relates.
+   *
+   * @return bool
+   *   Whether any fatal errors were found.
+   *
+   * @internal
+   */
+  public function logRead($test_id, $test_class) {
+    $log = DRUPAL_ROOT . '/' . $this->getTestSitePath() . '/error.log';
+    $found = FALSE;
+    if (file_exists($log)) {
+      foreach (file($log) as $line) {
+        if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
+          // Parse PHP fatal errors for example: PHP Fatal error: Call to
+          // undefined function break_me() in /path/to/file.php on line 17
+          $caller = [
+            'line' => $match[4],
+            'file' => $match[3],
+          ];
+          static::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
+        }
+        else {
+          // Unknown format, place the entire message in the log.
+          static::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error');
+        }
+        $found = TRUE;
+      }
+    }
+    return $found;
+  }
+
+  /**
+   * Defines the database schema for run-tests.sh and simpletest module.
+   *
+   * @return array
+   *   Array suitable for use in a hook_schema() implementation.
+   *
+   * @internal
+   */
+  public static function testingSchema() {
+    $schema['simpletest'] = [
+      'description' => 'Stores simpletest messages',
+      'fields' => [
+        'message_id' => [
+          'type' => 'serial',
+          'not null' => TRUE,
+          'description' => 'Primary Key: Unique simpletest message ID.',
+        ],
+        'test_id' => [
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Test ID, messages belonging to the same ID are reported together',
+        ],
+        'test_class' => [
+          'type' => 'varchar_ascii',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'The name of the class that created this message.',
+        ],
+        'status' => [
+          'type' => 'varchar',
+          'length' => 9,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'Message status. Core understands pass, fail, exception.',
+        ],
+        'message' => [
+          'type' => 'text',
+          'not null' => TRUE,
+          'description' => 'The message itself.',
+        ],
+        'message_group' => [
+          'type' => 'varchar_ascii',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'The message group this message belongs to. For example: warning, browser, user.',
+        ],
+        'function' => [
+          'type' => 'varchar_ascii',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'Name of the assertion function or method that created this message.',
+        ],
+        'line' => [
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Line number on which the function is called.',
+        ],
+        'file' => [
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'Name of the file where the function is called.',
+        ],
+      ],
+      'primary key' => ['message_id'],
+      'indexes' => [
+        'reporter' => ['test_class', 'message_id'],
+      ],
+    ];
+    $schema['simpletest_test_id'] = [
+      'description' => 'Stores simpletest test IDs, used to auto-increment the test ID so that a fresh test ID is used.',
+      'fields' => [
+        'test_id' => [
+          'type' => 'serial',
+          'not null' => TRUE,
+          'description' => 'Primary Key: Unique simpletest ID used to group test results together. Each time a set of tests
+                            are run a new test ID is used.',
+        ],
+        'last_prefix' => [
+          'type' => 'varchar',
+          'length' => 60,
+          'not null' => FALSE,
+          'default' => '',
+          'description' => 'The last database prefix used during testing.',
+        ],
+      ],
+      'primary key' => ['test_id'],
+    ];
+    return $schema;
+  }
+
 }
diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install
index fa63fb8269aeb4547c437a380e9888a704da5b93..424502789135c6fde9eb32d3b2d233704b7e65fd 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -7,6 +7,7 @@
 
 use Drupal\Component\Utility\Environment;
 use Drupal\Core\File\Exception\FileException;
+use Drupal\Core\Test\TestDatabase;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -78,92 +79,7 @@ function simpletest_requirements($phase) {
  * Implements hook_schema().
  */
 function simpletest_schema() {
-  $schema['simpletest'] = [
-    'description' => 'Stores simpletest messages',
-    'fields' => [
-      'message_id'  => [
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique simpletest message ID.',
-      ],
-      'test_id' => [
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Test ID, messages belonging to the same ID are reported together',
-      ],
-      'test_class' => [
-        'type' => 'varchar_ascii',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The name of the class that created this message.',
-      ],
-      'status' => [
-        'type' => 'varchar',
-        'length' => 9,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Message status. Core understands pass, fail, exception.',
-      ],
-      'message' => [
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => 'The message itself.',
-      ],
-      'message_group' => [
-        'type' => 'varchar_ascii',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The message group this message belongs to. For example: warning, browser, user.',
-      ],
-      'function' => [
-        'type' => 'varchar_ascii',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Name of the assertion function or method that created this message.',
-      ],
-      'line' => [
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Line number on which the function is called.',
-      ],
-      'file' => [
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Name of the file where the function is called.',
-      ],
-    ],
-    'primary key' => ['message_id'],
-    'indexes' => [
-      'reporter' => ['test_class', 'message_id'],
-    ],
-  ];
-  $schema['simpletest_test_id'] = [
-    'description' => 'Stores simpletest test IDs, used to auto-increment the test ID so that a fresh test ID is used.',
-    'fields' => [
-      'test_id'  => [
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique simpletest ID used to group test results together. Each time a set of tests
-                            are run a new test ID is used.',
-      ],
-      'last_prefix' => [
-        'type' => 'varchar',
-        'length' => 60,
-        'not null' => FALSE,
-        'default' => '',
-        'description' => 'The last database prefix used during testing.',
-      ],
-    ],
-    'primary key' => ['test_id'],
-  ];
-  return $schema;
+  return TestDatabase::testingSchema();
 }
 
 /**
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index ed3641239c807224aa9906ee95eb61c8babce359..da56752cb5b7cfa19633448ad4f74a32b189ba69 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -490,8 +490,8 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
     // Retrieve the last database prefix used for testing and the last test
     // class that was run from. Use the information to read the lgo file
     // in case any fatal errors caused the test to crash.
-    list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
-    simpletest_log_read($test_id, $last_prefix, $last_test_class);
+    list($last_prefix, $last_test_class) = TestDatabase::lastTestGet($test_id);
+    (new TestDatabase($last_prefix))->logRead($test_id, $last_test_class);
 
     \Drupal::messenger()->addError(t('The test run did not successfully finish.'));
     \Drupal::messenger()->addWarning(t('Use the <em>Clean environment</em> button to clean-up temporary files and tables.'));
@@ -507,19 +507,15 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
  * @return array
  *   Array containing the last database prefix used and the last test class
  *   that ran.
+ *
+ * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
+ *   \Drupal\Core\Test\TestDatabase::lastTestGet() instead.
+ *
+ * @see https://www.drupal.org/node/3075252
  */
 function simpletest_last_test_get($test_id) {
-  $last_prefix = TestDatabase::getConnection()
-    ->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, [
-      ':test_id' => $test_id,
-    ])
-    ->fetchField();
-  $last_test_class = TestDatabase::getConnection()
-    ->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, [
-      ':test_id' => $test_id,
-    ])
-    ->fetchField();
-  return [$last_prefix, $last_test_class];
+  @trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::lastTestGet() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
+  return TestDatabase::lastTestGet($test_id);
 }
 
 /**
@@ -537,30 +533,16 @@ function simpletest_last_test_get($test_id) {
  *
  * @return bool
  *   Whether any fatal errors were found.
+ *
+ * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
+ *   \Drupal\Core\Test\TestDatabase::logRead() instead.
+ *
+ * @see https://www.drupal.org/node/3075252
  */
 function simpletest_log_read($test_id, $database_prefix, $test_class) {
+  @trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::logRead() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
   $test_db = new TestDatabase($database_prefix);
-  $log = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/error.log';
-  $found = FALSE;
-  if (file_exists($log)) {
-    foreach (file($log) as $line) {
-      if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
-        // Parse PHP fatal errors for example: PHP Fatal error: Call to
-        // undefined function break_me() in /path/to/file.php on line 17
-        $caller = [
-          'line' => $match[4],
-          'file' => $match[3],
-        ];
-        simpletest_insert_assert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
-      }
-      else {
-        // Unknown format, place the entire message in the log.
-        simpletest_insert_assert($test_id, $test_class, FALSE, $line, 'Fatal error');
-      }
-      $found = TRUE;
-    }
-  }
-  return $found;
+  return $test_db->logRead($test_id, $test_class);
 }
 
 /**
@@ -589,34 +571,15 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
  *
  * @return
  *   Message ID of the stored assertion.
+ *
+ * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
+ *   \Drupal\Core\Test\TestDatabase::insertAssert() instead.
+ *
+ * @see https://www.drupal.org/node/3075252
  */
 function simpletest_insert_assert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = []) {
-  // Convert boolean status to string status.
-  if (is_bool($status)) {
-    $status = $status ? 'pass' : 'fail';
-  }
-
-  $caller += [
-    'function' => 'Unknown',
-    'line' => 0,
-    'file' => 'Unknown',
-  ];
-
-  $assertion = [
-    'test_id' => $test_id,
-    'test_class' => $test_class,
-    'status' => $status,
-    'message' => $message,
-    'message_group' => $group,
-    'function' => $caller['function'],
-    'line' => $caller['line'],
-    'file' => $caller['file'],
-  ];
-
-  return TestDatabase::getConnection()
-    ->insert('simpletest')
-    ->fields($assertion)
-    ->execute();
+  @trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::insertAssert() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
+  TestDatabase::insertAssert($test_id, $test_class, $status, $message, $group, $caller);
 }
 
 /**
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index 9856e7fe4b38e9b9ff98a9482f496f5f96fc3926..c3d5495fd00d137875a74e4248b238f1ad66d7eb 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -1252,7 +1252,7 @@ private function restoreEnvironment() {
 
     // In case a fatal error occurred that was not in the test process read the
     // log to pick up any fatal errors.
-    simpletest_log_read($this->testId, $this->databasePrefix, get_class($this));
+    (new TestDatabase($this->databasePrefix))->logRead($this->testId, get_class($this));
 
     // Restore original dependency injection container.
     $this->container = $this->originalContainer;
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 7b2a0088b6f79f7da2e9d6ac16719c2ae4fe1eca..3039fab006e3136bb794ad47903a2e26f822ad34 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -655,8 +655,7 @@ function simpletest_script_setup_database($new = FALSE) {
     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
   if ($new && $sqlite) {
-    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
-    foreach (simpletest_schema() as $name => $table_spec) {
+    foreach (TestDatabase::testingSchema() as $name => $table_spec) {
       try {
         $table_exists = $schema->tableExists($name);
         if (empty($args['keep-results-table']) && $table_exists) {
@@ -752,14 +751,14 @@ function simpletest_script_execute_batch($test_classes) {
           // @see https://www.drupal.org/node/2780087
           $total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
           // Insert a fail for xml results.
-          simpletest_insert_assert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
+          TestDatabase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
           // Ensure that an error line is displayed for the class.
           simpletest_script_reporter_display_summary(
             $child['class'],
             ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
           );
           if ($args['die-on-fail']) {
-            list($db_prefix) = simpletest_last_test_get($child['test_id']);
+            list($db_prefix) = TestDatabase::lastTestGet($child['test_id']);
             $test_db = new TestDatabase($db_prefix);
             $test_directory = $test_db->getTestSitePath();
             echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n";
@@ -910,7 +909,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
   }
   // Retrieve the last database prefix used for testing.
   try {
-    list($db_prefix) = simpletest_last_test_get($test_id);
+    list($db_prefix) = TestDatabase::lastTestGet($test_id);
   }
   catch (Exception $e) {
     echo (string) $e;
@@ -931,7 +930,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
 
   // Read the log file in case any fatal errors caused the test to crash.
   try {
-    simpletest_log_read($test_id, $db_prefix, $test_class);
+    (new TestDatabase($db_prefix))->logRead($test_id, $last_test_class);
   }
   catch (Exception $e) {
     echo (string) $e;
diff --git a/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php
index 6e9e63ddc3573f0ff00ee9f7145dbc6a50a21d62..9fc1e6f55411d785285b638c881f6c0c1383e200 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php
@@ -7,6 +7,9 @@
 
 /**
  * @coversDefaultClass \Drupal\Core\Test\TestDatabase
+ *
+ * @group Test
+ * @group simpletest
  * @group Template
  */
 class TestDatabaseTest extends UnitTestCase {
@@ -43,4 +46,29 @@ public function providerTestConstructor() {
     ];
   }
 
+  /**
+   * Verify that a test lock is generated if there is no provided prefix.
+   *
+   * @covers ::__construct
+   */
+  public function testConstructorNullPrefix() {
+    // We use a stub class here because we can't mock getTestLock() so that it's
+    // available before the constructor is called.
+    $test_db = new TestTestDatabase(NULL);
+
+    $this->assertEquals('test23', $test_db->getDatabasePrefix());
+    $this->assertEquals('sites/simpletest/23', $test_db->getTestSitePath());
+  }
+
+}
+
+/**
+ * Stub class supports TestDatabaseTest::testConstructorNullPrefix().
+ */
+class TestTestDatabase extends TestDatabase {
+
+  protected function getTestLock($create_lock = FALSE) {
+    return 23;
+  }
+
 }