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..3dc6500b20
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Setup/Commands/SetupDrupalTestScriptTest.php
@@ -0,0 +1,176 @@
+<?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
+ *
+ * @see \Drupal\Setup\Commands\TestInstallationSetupApplication
+ * @see \Drupal\Setup\Commands\TestInstallationSetupCommand
+ * @see \Drupal\Setup\Commands\TestTeardownCommand
+ */
+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 testInstallWithNonExistingClass() {
+    $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' => 'this-class-does-not-exist',
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+
+    $this->assertContains('There was a problem loading this-class-does-not-exist', $app_tester->getDisplay());
+  }
+
+  /**
+   * @coversNothing
+   */
+  public function testInstallWithNonSetupClass() {
+    $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' => static::class,
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+
+    $this->assertContains('You need to define a class implementing \Drupal\Setup\TestSetupInterface ', $app_tester->getDisplay());
+  }
+
+  /**
+   * @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());
+
+    $result = json_decode($app_tester->getDisplay(), TRUE);
+    $db_prefix = substr($result['db_prefix'], 4);
+    $this->assertNotEmpty($db_prefix);
+    $this->assertStringStartsWith('simpletest' . $db_prefix . ':', $result['user_agent']);
+
+    $this->assertEquals(0, $app_tester->getStatusCode());
+
+    $http_client = new Client();
+    $request = (new Request('GET', getenv('SIMPLETEST_BASE_URL') . '/test-page'))
+      ->withHeader('User-Agent', trim($result['user_agent']));
+
+    $response = $http_client->send($request);
+    // Ensure the test_page_test module got installed.
+    $this->assertContains('Test page | Drupal', (string) $response->getBody());
+
+    // Now test the tear down process as well.
+    $app_tester->run(
+      [
+        'command' => 'teardown-drupal-test',
+        'db_prefix' => $this->databasePrefix,
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+
+    // Ensure that all the tables for this DB prefix are gone.
+    $this->assertCount(0, \Drupal::database()->schema()->findTables($db_prefix . '%'));
+  }
+
+  /**
+   * @coversNothing
+   */
+  public function testInstallInDifferentLanguage() {
+    $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',
+        '--langcode' => 'fr',
+        '--setup_class' => SetupDrupalTestScript::class,
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+
+
+    $this->assertNotRegExp('/PHPUnit_Framework_Error_Warning/', $app_tester->getDisplay());
+    $this->assertNotRegExp('/AlreadyInstalledException/', $app_tester->getDisplay());
+    $this->assertEquals(0, $app_tester->getStatusCode());
+
+    $result = json_decode($app_tester->getDisplay(), TRUE);
+    $http_client = new Client();
+    $request = (new Request('GET', getenv('SIMPLETEST_BASE_URL') . '/test-page'))
+      ->withHeader('User-Agent', trim($result['user_agent']));
+
+    $response = $http_client->send($request);
+    // Ensure the test_page_test module got installed.
+    $this->assertContains('Test page | Drupal', (string) $response->getBody());
+    $this->assertContains('lang="fr"', (string) $response->getBody());
+
+    // Now test the tear down process as well.
+    $app_tester->run(
+      [
+        'command' => 'teardown-drupal-test',
+        'db_prefix' => $this->databasePrefix,
+      ],
+      [
+        'interactive' => FALSE,
+      ]
+    );
+  }
+
+}
diff --git a/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php
new file mode 100644
index 0000000000..15a5ab1804
--- /dev/null
+++ b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Setup\Commands;
+
+use Symfony\Component\Console\Application;
+
+/**
+ * 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 getDefaultCommands() {
+    // Even though this is a single command, keep the HelpCommand (--help).
+    $default_commands = parent::getDefaultCommands();
+    $default_commands[] = new TestInstallationSetupCommand($this->autoloader);
+    $default_commands[] = new TestTeardownCommand($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..d26024c3c2
--- /dev/null
+++ b/core/tests/Drupal/Setup/Commands/TestInstallationSetupCommand.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace Drupal\Setup\Commands;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Test\FunctionalTestSetupTrait;
+use Drupal\Core\Test\TestSetupTrait;
+use Drupal\Setup\TestInstallationSetup;
+use Drupal\Setup\TestSetupInterface;
+use Drupal\Tests\RandomGeneratorTrait;
+use Drupal\Tests\SessionTestTrait;
+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 {
+
+  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;
+
+
+  /**
+   * The language to install the site in.
+   *
+   * @var string
+   */
+  protected $langcode = 'en';
+
+  /**
+   * 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'))
+      ->addOption('install_profile', NULL, InputOption::VALUE_OPTIONAL, 'Install profile to install the site in. Defaults to testing', 'testing')
+      ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.')
+    ;
+  }
+
+  /**
+   * {@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();
+
+    // Manage site fixture.
+    $this->setup($input->getOption('install_profile'), $input->getOption('setup_class'), $input->getOption('langcode'));
+
+    $output->writeln(json_encode([
+      'db_prefix' => $this->databasePrefix,
+      'user_agent' => drupal_generate_test_ua($this->databasePrefix),
+    ]));
+  }
+
+  /**
+   * Bootstraps the drupal kernel.
+   */
+  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
+    );
+  }
+
+  /**
+   * 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.
+   * @param string $langcode
+   *   (optional) The language to install the site in.
+   */
+  public function setup($profile = 'testing', $setup_class = NULL, $langcode = 'en') {
+    $this->profile = $profile;
+    $this->langcode = $langcode;
+    $this->setupBaseUrl();
+    $this->prepareEnvironment();
+    $this->installDrupal();
+
+    if ($setup_class) {
+      $this->executeSetupClass($setup_class);
+    }
+  }
+
+  /**
+   * Ensures test files are deletable within file_unmanaged_delete_recursive().
+   *
+   * Some tests chmod generated files to be read only. During
+   * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
+   * these files need to get deleted too.
+   *
+   * @param string $path
+   *   The file path.
+   */
+  public static function filePreDeleteCallback($path) {
+    // When the webserver runs with the same system user as phpunit, we can
+    // make read-only files writable again. If not, chmod will fail while the
+    // file deletion still works if file permissions have been configured
+    // correctly. Thus, we ignore any problems while running chmod.
+    @chmod($path, 0700);
+  }
+
+  /**
+   * 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' => $this->langcode,
+      ],
+      '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/Commands/TestTeardownCommand.php b/core/tests/Drupal/Setup/Commands/TestTeardownCommand.php
new file mode 100644
index 0000000000..2acd50eb8d
--- /dev/null
+++ b/core/tests/Drupal/Setup/Commands/TestTeardownCommand.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\Setup\Commands;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Test\TestDatabase;
+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 tear down Drupal.
+ *
+ * @internal
+ */
+class TestTeardownCommand extends Command {
+
+  /**
+   * The used PHP autoloader.
+   *
+   * @var object
+   */
+  protected $autoloader;
+
+  /**
+   * Constructs a new TestTeardownCommand.
+   *
+   * @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('teardown-drupal-test')
+      ->addArgument('db_prefix')
+      ->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');
+    $db_prefix = $input->getArgument('db_prefix');
+    $base_url = $input->getOption('base_url');
+    putenv("SIMPLETEST_DB=$db_url");
+    putenv("SIMPLETEST_BASE_URL=$base_url");
+
+    $this->bootstrapDrupal($db_prefix);
+
+    // Handle the cleanup of the test site.
+    $this->teardown($db_prefix);
+  }
+
+  /**
+   * Removes a given instance by deleting all the database tables.
+   *
+   * @param string $db_prefix
+   *   The used database prefix.
+   */
+  public function teardown($db_prefix) {
+    $tables = Database::getConnection()->schema()->findTables('%');
+    foreach ($tables as $table) {
+      if (Database::getConnection()->schema()->dropTable($table)) {
+        unset($tables[$table]);
+      }
+    }
+
+    // Delete test site directory.
+    $test_database = new TestDatabase($db_prefix);
+    file_unmanaged_delete_recursive($test_database->getTestSitePath(), [$this, 'filePreDeleteCallback']);
+  }
+
+  /**
+   * Bootstraps the drupal kernel.
+   *
+   * @param string $db_prefix
+   *   The database prefix.
+   */
+  protected function bootstrapDrupal($db_prefix) {
+    $request = Request::createFromGlobals();
+    $_COOKIE['SIMPLETEST_USER_AGENT'] = drupal_generate_test_ua($db_prefix);
+
+    $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/TestSetupInterface.php b/core/tests/Drupal/Setup/TestSetupInterface.php
new file mode 100644
index 0000000000..5cd60c0df2
--- /dev/null
+++ b/core/tests/Drupal/Setup/TestSetupInterface.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Setup;
+
+/**
+ * Allows you to setup an environment used for javascript tests.
+ */
+interface TestSetupInterface {
+
+  /**
+   * Run the code to setup the test environment.
+   *
+   * 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__);
