diff --git a/core/scripts/setup-drupal-test.php b/core/scripts/setup-drupal-test.php
new file mode 100644
index 0000000000..304e0377c1
--- /dev/null
+++ b/core/scripts/setup-drupal-test.php
@@ -0,0 +1,20 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * @file
+ * A command line application to install drupal for tests.
+ */
+
+use Drupal\Setup\Commands\TestInstallationSetupApplication;
+
+if (PHP_SAPI !== 'cli') {
+  return;
+}
+
+// Bootstrap.
+$autoloader = require __DIR__ . '/../../autoload.php';
+require_once __DIR__ . '/../tests/bootstrap.php';
+
+$app = new TestInstallationSetupApplication($autoloader);
+$app->run();
diff --git a/core/tests/Drupal/KernelTests/Setup/Commands/SetupDrupalTestScriptTest.php b/core/tests/Drupal/KernelTests/Setup/Commands/SetupDrupalTestScriptTest.php
new file mode 100644
index 0000000000..0dcef01615
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Setup/Commands/SetupDrupalTestScriptTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\KernelTests\Setup\Commands;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Setup\Commands\TestInstallationSetupApplication;
+use Drupal\Setup\SetupDrupalTestScript;
+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\Request;
+use Symfony\Component\Console\Tester\ApplicationTester;
+
+/**
+ * Tests setup-drupal-test.php.
+ *
+ * @group Setup
+ *
+ * @todo Move this to the \Drupal\KernelTests\Setup\Commands\ namespace after
+ *   https://www.drupal.org/project/drupal/issues/2878269
+ */
+class SetupDrupalTestScriptTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    // Disable the usual kernel test setup process, as we want to run a custom
+    // command to install Drupal.
+    $this->root = static::getDrupalRoot();
+  }
+
+  /**
+   * @coversNothing
+   */
+  public function testInstallScript() {
+    $autoloader = $this->root . '/autoload.php';
+    $app = new TestInstallationSetupApplication(require $autoloader);
+    $app->setAutoExit(FALSE);
+
+    $app_tester = new ApplicationTester($app);
+    $app_tester->run(
+      [
+        'command' => 'setup-drupal-test',
+        '--setup_class' => SetupDrupalTestScript::class,
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+
+    $this->assertNotRegExp('/PHPUnit_Framework_Error_Warning/', $app_tester->getDisplay());
+    $this->assertNotRegExp('/AlreadyInstalledException/', $app_tester->getDisplay());
+    $this->assertRegExp('/simpletest/', $app_tester->getDisplay());
+    $this->assertEqual(0, $app_tester->getStatusCode());
+
+    $http_client = new Client();
+    $request = (new Request('GET', getenv('SIMPLETEST_BASE_URL') . '/test-page'))
+      ->withHeader('User-Agent', trim($app_tester->getDisplay()));
+
+    $response = $http_client->send($request);
+    // Ensure the test_page_test module got installed.
+    $this->assertContains('Test page | Drupal', (string) $response->getBody());
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php
new file mode 100644
index 0000000000..9c205daf7c
--- /dev/null
+++ b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Setup\Commands;
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Input\InputInterface;
+
+/**
+ * Application wrapper for TestInstallationSetupCommand.
+ *
+ * @internal
+ */
+class TestInstallationSetupApplication extends Application {
+
+  /**
+   * The used PHP autoloader.
+   *
+   * @var object
+   */
+  protected $autoloader;
+
+  /**
+   * SetupDrupalApplication constructor.
+   *
+   * @param string $autoloader
+   *   The used PHP autoloader.
+   */
+  public function __construct($autoloader) {
+    $this->autoloader = $autoloader;
+    parent::__construct('setup-drupal-test', '0.0.1');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCommandName(InputInterface $input) {
+    return 'setup-drupal-test';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultCommands() {
+    // Even though this is a single command, keep the HelpCommand (--help).
+    $default_commands = parent::getDefaultCommands();
+    $default_commands[] = new TestInstallationSetupCommand($this->autoloader);
+    return $default_commands;
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/Commands/TestInstallationSetupCommand.php b/core/tests/Drupal/Setup/Commands/TestInstallationSetupCommand.php
new file mode 100644
index 0000000000..0b622a284a
--- /dev/null
+++ b/core/tests/Drupal/Setup/Commands/TestInstallationSetupCommand.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\Setup\Commands;
+
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Site\Settings;
+use Drupal\Setup\TestInstallationSetup;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Symfony console command to setup Drupal.
+ *
+ * @internal
+ */
+class TestInstallationSetupCommand extends Command {
+
+  /**
+   * The used PHP autoloader.
+   *
+   * @var object
+   */
+  protected $autoloader;
+
+  /**
+   * Constructs a new TestInstallationSetupCommand.
+   *
+   * @param string $autoloader
+   *   The used PHP autoloader.
+   * @param string|null $name
+   *   The name of the command. Passing NULL means it must be set in
+   *   configure().
+   */
+  public function __construct($autoloader, $name = NULL) {
+    parent::__construct($name);
+
+    $this->autoloader = $autoloader;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    $this->setName('setup-drupal-test')
+      ->addOption('setup_class', NULL, InputOption::VALUE_OPTIONAL)
+      ->addOption('db_url', NULL, InputOption::VALUE_OPTIONAL, 'URL for database or SIMPLETEST_DB', getenv('SIMPLETEST_DB'))
+      ->addOption('base_url', NULL, InputOption::VALUE_OPTIONAL, 'Base URL for site under test or SIMPLETEST_BASE_URL', getenv('SIMPLETEST_BASE_URL'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    $db_url = $input->getOption('db_url');
+    $base_url = $input->getOption('base_url');
+    putenv("SIMPLETEST_DB=$db_url");
+    putenv("SIMPLETEST_BASE_URL=$base_url");
+
+    $this->bootstrapDrupal($this->autoloader);
+
+    // Manage site fixture.
+    $test = new TestInstallationSetup();
+    $test->setup('testing', $input->getOption('setup_class'));
+
+    $output->writeln(drupal_generate_test_ua($test->getDatabasePrefix()));
+  }
+
+  protected function bootstrapDrupal() {
+    $request = Request::createFromGlobals();
+    $kernel = DrupalKernel::createFromRequest($request, $this->autoloader, $this->getApplication()->getName());
+    DrupalKernel::bootEnvironment($kernel->getAppRoot());
+
+    Settings::initialize(
+      dirname(dirname(dirname(dirname(__DIR__)))),
+      DrupalKernel::findSitePath($request),
+      $this->autoloader
+    );
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/SetupDrupalTestScript.php b/core/tests/Drupal/Setup/SetupDrupalTestScript.php
new file mode 100644
index 0000000000..d8f7e1fbc9
--- /dev/null
+++ b/core/tests/Drupal/Setup/SetupDrupalTestScript.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\Setup;
+
+/**
+ * Setup file used by \Drupal\KernelTests\Setup\Commands\SetupDrupalTestScriptTest
+ */
+class SetupDrupalTestScript implements TestSetupInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setup() {
+    \Drupal::service('module_installer')->install(['test_page_test']);
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/TestInstallationSetup.php b/core/tests/Drupal/Setup/TestInstallationSetup.php
new file mode 100644
index 0000000000..50b027a10a
--- /dev/null
+++ b/core/tests/Drupal/Setup/TestInstallationSetup.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Drupal\Setup;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Test\FunctionalTestSetupTrait;
+use Drupal\Core\Test\TestSetupTrait;
+use Drupal\Tests\RandomGeneratorTrait;
+use Drupal\Tests\SessionTestTrait;
+
+/**
+ * Provides a class used by setup-drupal-test.php to install Drupal for tests.
+ *
+ * @internal
+ */
+class TestInstallationSetup {
+
+  use FunctionalTestSetupTrait;
+  use RandomGeneratorTrait;
+  use SessionTestTrait;
+  use TestSetupTrait;
+
+  /**
+   * The install profile to use.
+   *
+   * @var string
+   */
+  protected $profile;
+
+  /**
+   * Time limit in seconds for the test.
+   *
+   * @var int
+   */
+  protected $timeLimit = 500;
+
+  /**
+   * The database prefix of this test run.
+   *
+   * @var string
+   */
+  protected $databasePrefix;
+
+  /**
+   * Creates a test drupal installation.
+   *
+   * @param string $profile
+   *   (optional) The installation profile to use.
+   * @param string $setup_class
+   *   (optional) Setup class. A PHP class to setup configuration used by the
+   *   test.
+   */
+  public function setup($profile = 'testing', $setup_class = NULL) {
+    $this->profile = $profile;
+    $this->setupBaseUrl();
+    $this->prepareEnvironment();
+    $this->installDrupal();
+
+    if ($setup_class) {
+      $this->executeSetupClass($setup_class);
+    }
+  }
+
+  /**
+   * Gets the database prefix.
+   *
+   * @return string
+   */
+  public function getDatabasePrefix() {
+    return $this->databasePrefix;
+  }
+
+  /**
+   * Installs Drupal into the Simpletest site.
+   */
+  protected function installDrupal() {
+    $this->initUserSession();
+    $this->prepareSettings();
+    $this->doInstall();
+    $this->initSettings();
+    $container = $this->initKernel(\Drupal::request());
+    $this->initConfig($container);
+    $this->installModulesFromClassProperty($container);
+    $this->rebuildAll();
+  }
+
+  /**
+   * Uses the setup file to configure Drupal.
+   *
+   * @param string $class
+   *   The full qualified class name, which should setup Drupal for tests. One common need for
+   *   example would be to create the required content types and fields.
+   *   The class needs to implement \Drupal\Setup\TestSetupInterface
+   *
+   * @see \Drupal\Setup\TestSetupInterface
+   */
+  protected function executeSetupClass($class) {
+    if (!class_exists($class)) {
+      throw new \InvalidArgumentException("There was a problem loading {$class}");
+    }
+
+    if (!is_subclass_of($class, TestSetupInterface::class)) {
+      throw new \InvalidArgumentException(sprintf('You need to define a class implementing \Drupal\Setup\TestSetupInterface'));
+    }
+
+    /** @var \Drupal\Setup\TestSetupInterface $instance */
+    $instance = new $class;
+    $instance->setup();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function installParameters() {
+    $connection_info = Database::getConnectionInfo();
+    $driver = $connection_info['default']['driver'];
+    $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
+    unset($connection_info['default']['driver']);
+    unset($connection_info['default']['namespace']);
+    unset($connection_info['default']['pdo']);
+    unset($connection_info['default']['init_commands']);
+    $parameters = [
+      'interactive' => FALSE,
+      'parameters' => [
+        'profile' => $this->profile,
+        'langcode' => 'en',
+      ],
+      'forms' => [
+        'install_settings_form' => [
+          'driver' => $driver,
+          $driver => $connection_info['default'],
+        ],
+        'install_configure_form' => [
+          'site_name' => 'Drupal',
+          'site_mail' => 'simpletest@example.com',
+          'account' => [
+            'name' => $this->rootUser->name,
+            'mail' => $this->rootUser->getEmail(),
+            'pass' => [
+              'pass1' => $this->rootUser->pass_raw,
+              'pass2' => $this->rootUser->pass_raw,
+            ],
+          ],
+          // form_type_checkboxes_value() requires NULL instead of FALSE values
+          // for programmatic form submissions to disable a checkbox.
+          'enable_update_status_module' => NULL,
+          'enable_update_status_emails' => NULL,
+        ],
+      ],
+    ];
+    return $parameters;
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/TestSetupInterface.php b/core/tests/Drupal/Setup/TestSetupInterface.php
new file mode 100644
index 0000000000..fe992161dc
--- /dev/null
+++ b/core/tests/Drupal/Setup/TestSetupInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\Setup;
+
+/**
+ * Allows you to setup an environment used for javascript tests.
+ */
+interface TestSetupInterface {
+
+  /**
+   * Run code to setup the test.
+   *
+   * You have access to any API provided by any installed module. To install
+   * modules use
+   * @code
+   * \Drupal::service('module_installer')->install(['my_module'])
+   * @endcode
+   *
+   * Check out 'core/tests/Drupal/Setup/SetupDrupalTestScript.php' for an example.
+   */
+  public function setup();
+
+}
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index 50fef61ee6..17dc860dc1 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -128,6 +128,7 @@ function drupal_phpunit_populate_class_loader() {
 
   // Start with classes in known locations.
   $loader->add('Drupal\\Tests', __DIR__);
+  $loader->add('Drupal\\Setup', __DIR__);
   $loader->add('Drupal\\KernelTests', __DIR__);
   $loader->add('Drupal\\FunctionalTests', __DIR__);
   $loader->add('Drupal\\FunctionalJavascriptTests', __DIR__);
