diff --git a/core/lib/Drupal/Core/Test/TestDatabase.php b/core/lib/Drupal/Core/Test/TestDatabase.php
index 20cc8e6687..cb964b769c 100644
--- a/core/lib/Drupal/Core/Test/TestDatabase.php
+++ b/core/lib/Drupal/Core/Test/TestDatabase.php
@@ -179,64 +179,6 @@ 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.
    *
@@ -288,15 +230,23 @@ public function logRead($test_id, $test_class) {
         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 = [
+          TestRun::get(static::getConnection(), $test_id)->insertLogEntry([
+            'test_class' => $test_class,
+            'status' => 'fail',
+            'message' => $match[2],
+            'message_group' => $match[1],
             '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');
+          TestRun::get(static::getConnection(), $test_id)->insertLogEntry([
+            'test_class' => $test_class,
+            'status' => 'fail',
+            'message' => $line,
+            'message_group' => 'Fatal error',
+          ]);
         }
         $found = TRUE;
       }
diff --git a/core/lib/Drupal/Core/Test/TestRun.php b/core/lib/Drupal/Core/Test/TestRun.php
new file mode 100644
index 0000000000..6b5fd5b1e1
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/TestRun.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Core\Test;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ *
+ */
+class TestRun {
+
+  protected $connection;
+  protected $testId;
+
+  /**
+   * TestRun constructor.
+   */
+  public function __construct(Connection $connection, $test_id) {
+    $this->connection = $connection;
+    $this->testId = $test_id;
+  }
+
+  /**
+   *
+   */
+  public static function createNew(Connection $connection) {
+    $test_id = $connection->insert('simpletest_test_id')
+      ->useDefaults(['test_id'])
+      ->execute();
+    return new static($connection, $test_id);
+  }
+
+  /**
+   *
+   */
+  public static function get(Connection $connection, $test_id) {
+    return new static($connection, $test_id);
+  }
+
+  /**
+   *
+   */
+  public function id() {
+    return $this->testId;
+  }
+
+  /**
+   *
+   */
+  public function update(array $status) {
+    $affected_rows = $this->connection->update('simpletest_test_id')
+      ->fields($status)
+      ->condition('test_id', $this->testId)
+      ->execute();
+    if (!$affected_rows) {
+      throw new \RuntimeException('Failed to set up database prefix.');
+    }
+  }
+
+  /**
+   *
+   */
+  public function insertLogEntry(array $entry) {
+    $entry['test_id'] = $this->testId;
+    $entry = array_merge([
+      'function' => 'Unknown',
+      'line' => 0,
+      'file' => 'Unknown',
+    ], $entry);
+
+    return $this->connection->insert('simpletest')
+      ->fields($entry)
+      ->execute();
+  }
+
+}
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index fc833d1046..a04e332c38 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -13,6 +13,7 @@
 use Drupal\Core\Test\JUnitConverter;
 use Drupal\Core\Test\PhpUnitTestRunner;
 use Drupal\Core\Test\TestDatabase;
+use Drupal\Core\Test\TestRun;
 use Drupal\Core\Url;
 use Drupal\simpletest\TestDiscovery;
 use PHPUnit\Framework\TestCase;
@@ -139,9 +140,7 @@ function simpletest_run_tests($test_list) {
     unset($test_list['phpunit']);
   }
 
-  $test_id = \Drupal::database()->insert('simpletest_test_id')
-    ->useDefaults(['test_id'])
-    ->execute();
+  $test_run = TestRun::createNew(\Drupal::database());
 
   // Clear out the previous verbose files.
   try {
@@ -158,7 +157,7 @@ function simpletest_run_tests($test_list) {
   $batch = [
     'title' => t('Running tests'),
     'operations' => [
-      ['_simpletest_batch_operation', [$test_list, $test_id]],
+      ['_simpletest_batch_operation', [$test_list, $test_run->id()]],
     ],
     'finished' => '_simpletest_batch_finished',
     'progress_message' => '',
@@ -169,7 +168,7 @@ function simpletest_run_tests($test_list) {
 
   \Drupal::moduleHandler()->invokeAll('test_group_started');
 
-  return $test_id;
+  return $test_run->id();
 }
 
 /**
@@ -209,13 +208,10 @@ function simpletest_process_phpunit_results($phpunit_results) {
   // Insert the results of the PHPUnit test run into the database so the results
   // are displayed along with Simpletest's results.
   if (!empty($phpunit_results)) {
-    $query = TestDatabase::getConnection()
-      ->insert('simpletest')
-      ->fields(array_keys($phpunit_results[0]));
+    $test_run = TestRun::get(TestDatabase::getConnection(), $phpunit_results[0]['test_id']);
     foreach ($phpunit_results as $result) {
-      $query->values($result);
+      $test_run->insertLogEntry($result);
     }
-    $query->execute();
   }
 }
 
@@ -496,7 +492,29 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
  */
 function simpletest_insert_assert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = []) {
   @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);
+
+  // Convert boolean status to string status.
+  if (is_bool($status)) {
+    $status = $status ? 'pass' : 'fail';
+  }
+
+  $caller += [
+    'function' => 'Unknown',
+    'line' => 0,
+    'file' => 'Unknown',
+  ];
+
+  $assertion = [
+    'test_class' => $test_class,
+    'status' => $status,
+    'message' => $message,
+    'message_group' => $group,
+    'function' => $caller['function'],
+    'line' => $caller['line'],
+    'file' => $caller['file'],
+  ];
+
+  TestRun::get(TestDatabase::getConnection(), $test_id)->insertLogEntry($assertion);
 }
 
 /**
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index c3d5495fd0..eadb551dcd 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -13,6 +13,7 @@
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Test\TestDiscovery;
+use Drupal\Core\Test\TestRun;
 use Drupal\Core\Test\TestSetupTrait;
 use Drupal\Core\Utility\Error;
 use Drupal\Tests\AssertHelperTrait as BaseAssertHelperTrait;
@@ -296,10 +297,7 @@ protected function checkRequirements() {
    *   The message ID.
    */
   protected function storeAssertion(array $assertion) {
-    return self::getDatabaseConnection()
-      ->insert('simpletest', ['return' => Database::RETURN_INSERT_ID])
-      ->fields($assertion)
-      ->execute();
+    return TestRun::get(self::getDatabaseConnection(), $this->testId)->insertLogEntry($assertion);
   }
 
   /**
@@ -416,10 +414,7 @@ public static function insertAssert($test_id, $test_class, $status, $message = '
     ];
 
     // We can't use storeAssertion() because this method is static.
-    return self::getDatabaseConnection()
-      ->insert('simpletest')
-      ->fields($assertion)
-      ->execute();
+    return TestRun::get(self::getDatabaseConnection(), $test_id)->insertLogEntry($assertion);
   }
 
   /**
@@ -1041,13 +1036,7 @@ private function prepareDatabasePrefix() {
     // As soon as the database prefix is set, the test might start to execute.
     // All assertions as well as the SimpleTest batch operations are associated
     // with the testId, so the database prefix has to be associated with it.
-    $affected_rows = self::getDatabaseConnection()->update('simpletest_test_id')
-      ->fields(['last_prefix' => $this->databasePrefix])
-      ->condition('test_id', $this->testId)
-      ->execute();
-    if (!$affected_rows) {
-      throw new \RuntimeException('Failed to set up database prefix.');
-    }
+    TestRun::get(self::getDatabaseConnection(), $this->testId)->update(['last_prefix' => $this->databasePrefix]);
   }
 
   /**
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 26763c3adb..3e6c6c3ef3 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -16,6 +16,7 @@
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\PhpUnitTestRunner;
 use Drupal\Core\Test\TestDatabase;
+use Drupal\Core\Test\TestRun;
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\simpletest\Form\SimpletestResultsForm;
 use Drupal\Core\Test\TestDiscovery;
@@ -692,20 +693,17 @@ function simpletest_script_execute_batch($test_classes) {
       }
 
       try {
-        $test_id = Database::getConnection('default', 'test-runner')
-          ->insert('simpletest_test_id')
-          ->useDefaults(['test_id'])
-          ->execute();
+        $test_run = TestRun::createNew(Database::getConnection('default', 'test-runner'));
       }
       catch (Exception $e) {
         echo (string) $e;
         exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
       }
-      $test_ids[] = $test_id;
+      $test_ids[] = $test_run->id();
 
       $test_class = array_shift($test_classes);
       // Fork a child process.
-      $command = simpletest_script_command($test_id, $test_class);
+      $command = simpletest_script_command($test_run->id(), $test_class);
       $process = proc_open($command, [], $pipes, NULL, NULL, ['bypass_shell' => TRUE]);
 
       if (!is_resource($process)) {
@@ -716,7 +714,7 @@ function simpletest_script_execute_batch($test_classes) {
       // Register our new child.
       $children[] = [
         'process' => $process,
-        'test_id' => $test_id,
+        'test_run' => $test_run,
         'class' => $test_class,
         'pipes' => $pipes,
       ];
@@ -742,14 +740,19 @@ 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.
-          TestDatabase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
+          $child['test_run']->insertLogEntry([
+            'test_class' => $child['class'],
+            'status' => 'fail',
+            'message' => $message,
+            'message_group' => '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) = TestDatabase::lastTestGet($child['test_id']);
+            list($db_prefix) = TestDatabase::lastTestGet($child['test_run']->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";
@@ -760,7 +763,7 @@ function simpletest_script_execute_batch($test_classes) {
         }
         // Free-up space by removing any potentially created resources.
         if (!$args['keep-results']) {
-          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
+          simpletest_script_cleanup($child['test_run']->id(), $child['class'], $status['exitcode']);
         }
 
         // Remove this child.
