diff --git a/core/lib/Drupal/Core/Test/AssertMailTrait.php b/core/lib/Drupal/Core/Test/AssertMailTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..f6b5279ca91f1290cb1c0418c05d651888171d56
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/AssertMailTrait.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Test\AssertMailTrait.
+ */
+
+namespace Drupal\Core\Test;
+
+/**
+ * Provides methods for testing emails sent during test runs.
+ */
+trait AssertMailTrait {
+
+  /**
+   * Gets an array containing all emails sent during this test case.
+   *
+   * @param array $filter
+   *   An array containing key/value pairs used to filter the emails that are
+   *   returned.
+   *
+   * @return array
+   *   An array containing email messages captured during the current test.
+   */
+  protected function getMails(array $filter = []) {
+    $captured_emails = $this->container->get('state')->get('system.test_mail_collector', []);
+    $filtered_emails = [];
+
+    foreach ($captured_emails as $message) {
+      foreach ($filter as $key => $value) {
+        if (!isset($message[$key]) || $message[$key] != $value) {
+          continue 2;
+        }
+      }
+      $filtered_emails[] = $message;
+    }
+
+    return $filtered_emails;
+  }
+
+  /**
+   * Asserts that the most recently sent email message has the given value.
+   *
+   * The field in $name must have the content described in $value.
+   *
+   * @param string $name
+   *   Name of field or message property to assert. Examples: subject, body,
+   *   id, ...
+   * @param string $value
+   *   Value of the field to assert.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   * @param string $group
+   *   (optional) The group this message is in, which is displayed in a column
+   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
+   *   translate this string. Defaults to 'Email'; most tests do not override
+   *   this default.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertMail($name, $value = '', $message = '', $group = 'Email') {
+    $captured_emails = $this->container->get('state')->get('system.test_mail_collector') ?: [];
+    $email = end($captured_emails);
+    return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, $group);
+  }
+
+  /**
+   * Asserts that the most recently sent email message has the string in it.
+   *
+   * @param string $field_name
+   *   Name of field or message property to assert: subject, body, id, ...
+   * @param string $string
+   *   String to search for.
+   * @param int $email_depth
+   *   Number of emails to search for string, starting with most recent.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   * @param string $group
+   *   (optional) The group this message is in, which is displayed in a column
+   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
+   *   translate this string. Defaults to 'Other'; most tests do not override
+   *   this default.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertMailString($field_name, $string, $email_depth, $message = '', $group = 'Other') {
+    $mails = $this->getMails();
+    $string_found = FALSE;
+    // Cast MarkupInterface objects to string.
+    $string = (string) $string;
+    for ($i = count($mails) - 1; $i >= count($mails) - $email_depth && $i >= 0; $i--) {
+      $mail = $mails[$i];
+      // Normalize whitespace, as we don't know what the mail system might have
+      // done. Any run of whitespace becomes a single space.
+      $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]);
+      $normalized_string = preg_replace('/\s+/', ' ', $string);
+      $string_found = (FALSE !== strpos($normalized_mail, $normalized_string));
+      if ($string_found) {
+        break;
+      }
+    }
+    if (!$message) {
+      $message = format_string('Expected text found in @field of email message: "@expected".', ['@field' => $field_name, '@expected' => $string]);
+    }
+    return $this->assertTrue($string_found, $message, $group);
+  }
+
+  /**
+   * Asserts that the most recently sent email message has the pattern in it.
+   *
+   * @param string $field_name
+   *   Name of field or message property to assert: subject, body, id, ...
+   * @param string $regex
+   *   Pattern to search for.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   * @param string $group
+   *   (optional) The group this message is in, which is displayed in a column
+   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
+   *   translate this string. Defaults to 'Other'; most tests do not override
+   *   this default.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertMailPattern($field_name, $regex, $message = '', $group = 'Other') {
+    $mails = $this->getMails();
+    $mail = end($mails);
+    $regex_found = preg_match("/$regex/", $mail[$field_name]);
+    if (!$message) {
+      $message = format_string('Expected text found in @field of email message: "@expected".', ['@field' => $field_name, '@expected' => $regex]);
+    }
+    return $this->assertTrue($regex_found, $message, $group);
+  }
+
+  /**
+   * Outputs to verbose the most recent $count emails sent.
+   *
+   * @param int $count
+   *   Optional number of emails to output.
+   */
+  protected function verboseEmail($count = 1) {
+    $mails = $this->getMails();
+    for ($i = count($mails) - 1; $i >= count($mails) - $count && $i >= 0; $i--) {
+      $mail = $mails[$i];
+      $this->verbose('Email:<pre>' . print_r($mail, TRUE) . '</pre>');
+    }
+  }
+
+}
diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php
index a987d13b41ed018a742d3c226ce1fd91f87f7992..681e75c1f1075bbf21b0e317a15f36f559e2030c 100644
--- a/core/modules/simpletest/src/KernelTestBase.php
+++ b/core/modules/simpletest/src/KernelTestBase.php
@@ -271,6 +271,12 @@ protected function setUp() {
     // The temporary stream wrapper is able to operate both with and without
     // configuration.
     $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
+
+    // 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.
+    $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
   }
 
   /**
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index e4e9622f64d1200a37cd0215b2a09d553b80d674..e9b63768202953adf89ea00a3c9fbd9b78f5d68f 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -28,6 +28,7 @@
 use Drupal\Core\Session\UserSession;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\StreamWrapper\PublicStream;
+use Drupal\Core\Test\AssertMailTrait;
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -47,6 +48,9 @@ abstract class WebTestBase extends TestBase {
   use ContentTypeCreationTrait {
     createContentType as drupalCreateContentType;
   }
+  use AssertMailTrait {
+    getMails as drupalGetMails;
+  }
   use NodeCreationTrait {
     getNodeByTitle as drupalGetNodeByTitle;
     createNode as drupalCreateNode;
@@ -2518,32 +2522,6 @@ protected function assertHeader($header, $value, $message = '', $group = 'Browse
     return $this->assertTrue($header_value == $value, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found, actual value: ' . $header_value, $group);
   }
 
-  /**
-   * Gets an array containing all emails sent during this test case.
-   *
-   * @param $filter
-   *   An array containing key/value pairs used to filter the emails that are
-   *   returned.
-   *
-   * @return
-   *   An array containing email messages captured during the current test.
-   */
-  protected function drupalGetMails($filter = array()) {
-    $captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array();
-    $filtered_emails = array();
-
-    foreach ($captured_emails as $message) {
-      foreach ($filter as $key => $value) {
-        if (!isset($message[$key]) || $message[$key] != $value) {
-          continue 2;
-        }
-      }
-      $filtered_emails[] = $message;
-    }
-
-    return $filtered_emails;
-  }
-
   /**
    * Passes if the internal browser's URL matches the given path.
    *
@@ -2644,126 +2622,6 @@ protected function assertNoResponse($code, $message = '', $group = 'Browser') {
     return $this->assertFalse($match, $message ? $message : SafeMarkup::format('HTTP response not expected @code, actual @curl_code', array('@code' => $code, '@curl_code' => $curl_code)), $group);
   }
 
-  /**
-   * Asserts that the most recently sent email message has the given value.
-   *
-   * The field in $name must have the content described in $value.
-   *
-   * @param $name
-   *   Name of field or message property to assert. Examples: subject, body,
-   *   id, ...
-   * @param $value
-   *   Value of the field to assert.
-   * @param $message
-   *   (optional) A message to display with the assertion. Do not translate
-   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
-   *   variables in the message text, not t(). If left blank, a default message
-   *   will be displayed.
-   * @param $group
-   *   (optional) The group this message is in, which is displayed in a column
-   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
-   *   translate this string. Defaults to 'Email'; most tests do not override
-   *   this default.
-   *
-   * @return
-   *   TRUE on pass, FALSE on fail.
-   */
-  protected function assertMail($name, $value = '', $message = '', $group = 'Email') {
-    $captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array();
-    $email = end($captured_emails);
-    return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, $group);
-  }
-
-  /**
-   * Asserts that the most recently sent email message has the string in it.
-   *
-   * @param $field_name
-   *   Name of field or message property to assert: subject, body, id, ...
-   * @param $string
-   *   String to search for.
-   * @param $email_depth
-   *   Number of emails to search for string, starting with most recent.
-   * @param $message
-   *   (optional) A message to display with the assertion. Do not translate
-   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
-   *   variables in the message text, not t(). If left blank, a default message
-   *   will be displayed.
-   * @param $group
-   *   (optional) The group this message is in, which is displayed in a column
-   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
-   *   translate this string. Defaults to 'Other'; most tests do not override
-   *   this default.
-   *
-   * @return
-   *   TRUE on pass, FALSE on fail.
-   */
-  protected function assertMailString($field_name, $string, $email_depth, $message = '', $group = 'Other') {
-    $mails = $this->drupalGetMails();
-    $string_found = FALSE;
-    // Cast MarkupInterface objects to string.
-    $string = (string) $string;
-    for ($i = count($mails) -1; $i >= count($mails) - $email_depth && $i >= 0; $i--) {
-      $mail = $mails[$i];
-      // Normalize whitespace, as we don't know what the mail system might have
-      // done. Any run of whitespace becomes a single space.
-      $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]);
-      $normalized_string = preg_replace('/\s+/', ' ', $string);
-      $string_found = (FALSE !== strpos($normalized_mail, $normalized_string));
-      if ($string_found) {
-        break;
-      }
-    }
-    if (!$message) {
-      $message = format_string('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $string));
-    }
-    return $this->assertTrue($string_found, $message, $group);
-  }
-
-  /**
-   * Asserts that the most recently sent email message has the pattern in it.
-   *
-   * @param $field_name
-   *   Name of field or message property to assert: subject, body, id, ...
-   * @param $regex
-   *   Pattern to search for.
-   * @param $message
-   *   (optional) A message to display with the assertion. Do not translate
-   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
-   *   variables in the message text, not t(). If left blank, a default message
-   *   will be displayed.
-   * @param $group
-   *   (optional) The group this message is in, which is displayed in a column
-   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
-   *   translate this string. Defaults to 'Other'; most tests do not override
-   *   this default.
-   *
-   * @return
-   *   TRUE on pass, FALSE on fail.
-   */
-  protected function assertMailPattern($field_name, $regex, $message = '', $group = 'Other') {
-    $mails = $this->drupalGetMails();
-    $mail = end($mails);
-    $regex_found = preg_match("/$regex/", $mail[$field_name]);
-    if (!$message) {
-      $message = format_string('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $regex));
-    }
-    return $this->assertTrue($regex_found, $message, $group);
-  }
-
-  /**
-   * Outputs to verbose the most recent $count emails sent.
-   *
-   * @param $count
-   *   Optional number of emails to output.
-   */
-  protected function verboseEmail($count = 1) {
-    $mails = $this->drupalGetMails();
-    for ($i = count($mails) -1; $i >= count($mails) - $count && $i >= 0; $i--) {
-      $mail = $mails[$i];
-      $this->verbose('Email:<pre>' . print_r($mail, TRUE) . '</pre>');
-    }
-  }
-
   /**
    * Creates a mock request and sets it on the generator.
    *
diff --git a/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php b/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7af7718d62bb7bd1778e2aa84cf1950b6c19e4df
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\KernelTests\Core\Test\AssertMailTraitTest.
+ */
+
+namespace Drupal\KernelTests\Core\Test;
+
+use Drupal\Core\Test\AssertMailTrait;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests \Drupal\Core\Test\AssertMailTrait works.
+ *
+ * @group Test
+ *
+ * @coversDefaultClass \Drupal\Core\Test\AssertMailTrait
+ */
+class AssertMailTraitTest extends KernelTestBase {
+  use AssertMailTrait;
+
+  /**
+   * Tests that the maintenance theme initializes the theme and its base themes.
+   */
+  public function testAssertMailTrait() {
+    /* @var \Drupal\Core\Mail\MailManagerInterface $mail_service */
+    $mail_service = \Drupal::service('plugin.manager.mail');
+
+    // Create an email.
+    $subject = $this->randomString(64);
+    $body = $this->randomString(128);
+    $message = [
+      'id' => 'drupal_mail_test',
+      'headers' => ['Content-type' => 'text/html'],
+      'subject' => $subject,
+      'to' => 'foobar@example.com',
+      'body' => $body,
+    ];
+
+    // Before we send the email, \Drupal\Core\Test\AssertMailTrait::getMails()
+    // should return an empty array.
+    $captured_emails = $this->getMails();
+    $this->assertCount(0, $captured_emails, 'The captured emails queue is empty.');
+
+    // Send the email.
+    $mail_service->getInstance(['module' => 'simpletest', 'key' => 'drupal_mail_test'])->mail($message);
+
+    // Ensure that there is one email in the captured emails array.
+    $captured_emails = $this->getMails();
+    $this->assertEquals(count($captured_emails), 1, 'One email was captured.');
+
+    // Assert that the email was sent by iterating over the message properties
+    // and ensuring that they are captured intact.
+    foreach ($message as $field => $value) {
+      $this->assertMail($field, $value, "The email was sent and the value for property $field is intact.");
+    }
+
+    // Send additional emails so more than one email is captured.
+    for ($index = 0; $index < 5; $index++) {
+      $message = [
+        'id' => 'drupal_mail_test_' . $index,
+        'headers' => ['Content-type' => 'text/html'],
+        'subject' => $this->randomString(64),
+        'to' => $this->randomMachineName(32) . '@example.com',
+        'body' => $this->randomString(512),
+      ];
+      $mail_service->getInstance(['module' => 'drupal_mail_test', 'key' => $index])->mail($message);
+    }
+
+    // There should now be 6 emails captured.
+    $captured_emails = $this->getMails();
+    $this->assertCount(6, $captured_emails, 'All emails were captured.');
+
+    // Test different ways of getting filtered emails via
+    // \Drupal\Core\Test\AssertMailTrait::getMails().
+    $captured_emails = $this->getMails(['id' => 'drupal_mail_test']);
+    $this->assertCount(1, $captured_emails, 'Only one email is returned when filtering by id.');
+    $captured_emails = $this->getMails(['id' => 'drupal_mail_test', 'subject' => $subject]);
+    $this->assertCount(1, $captured_emails, 'Only one email is returned when filtering by id and subject.');
+    $captured_emails = $this->getMails([
+      'id' => 'drupal_mail_test',
+      'subject' => $subject,
+      'from' => 'this_was_not_used@example.com',
+    ]);
+    $this->assertCount(0, $captured_emails, 'No emails are returned when querying with an unused from address.');
+
+    // Send the last email again, so we can confirm that
+    // \Drupal\Core\Test\AssertMailTrait::getMails() filters correctly returns
+    // all emails with a given property/value.
+    $mail_service->getInstance(['module' => 'drupal_mail_test', 'key' => $index])->mail($message);
+    $captured_emails = $this->getMails(['id' => 'drupal_mail_test_4']);
+    $this->assertCount(2, $captured_emails, 'All emails with the same id are returned when filtering by id.');
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index d20f341b38005f2519f97345052c05681f38e662..25ca3b7abe78fc2ab111225ff0c7f76cb0f3a400 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -370,6 +370,12 @@ private function bootKernel() {
       'class' => '\Drupal\Component\PhpStorage\FileStorage',
     ];
     new Settings($settings);
+
+    // 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.
+    $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
   }
 
   /**