diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 5a2bed573ab10f421304dbbca744ac6774a0b4ed..37c913ae9b82bba9b6c819a219415ae855a59e7c 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -620,7 +620,7 @@ public function processForm($form_id, &$form, &$form_state) {
         // possibly ending execution. We make sure we do not react to the batch
         // that is already being processed (if a batch operation performs a
         // self::submitForm).
-        if ($batch =& batch_get() && !isset($batch['current_set'])) {
+        if ($batch = &$this->batchGet() && !isset($batch['current_set'])) {
           // Store $form_state information in the batch definition.
           // We need the full $form_state when either:
           // - Some submit handlers were saved to be called during batch
@@ -1195,7 +1195,7 @@ public function executeHandlers($type, &$form, &$form_state) {
       // Check if a previous _submit handler has set a batch, but make sure we
       // do not react to a batch that is already being processed (for instance
       // if a batch operation performs a self::submitForm()).
-      if ($type == 'submit' && ($batch =& batch_get()) && !isset($batch['id'])) {
+      if ($type == 'submit' && ($batch = &$this->batchGet()) && !isset($batch['id'])) {
         // Some previous submit handler has set a batch. To ensure correct
         // execution order, store the call in a special 'control' batch set.
         // See _batch_next_set().
@@ -1837,4 +1837,11 @@ public function setRequest(Request $request) {
     $this->request = $request;
   }
 
+  /**
+   * Wraps batch_get().
+   */
+  protected function &batchGet() {
+    return batch_get();
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
index 65633397ec0d3ecfadbb5dde951a4d11c233149c..5424f66d19bc5de0115e3be40b809fdd4799a8fb 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -656,17 +656,6 @@ function testRequiredAttribute() {
     $this->assertTrue(!empty($element), 'The textarea has the proper required attribute.');
   }
 
-  /**
-   *  Tests error border of multiple fields with same name in a page.
-   */
-  function testMultiFormSameNameErrorClass() {
-    $this->drupalGet('form-test/double-form');
-    $edit = array();
-    $this->drupalPostForm(NULL, $edit, t('Save'));
-    $this->assertFieldByXpath('//input[@id="edit-name" and contains(@class, "error")]', NULL, 'Error input form element class found for first element.');
-    $this->assertNoFieldByXpath('//input[@id="edit-name--2" and contains(@class, "error")]', NULL, 'No error input form element class found for second element.');
-  }
-
   /**
    * Tests a form with a form state storing a database connection.
    */
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/HTMLIdTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/HTMLIdTest.php
deleted file mode 100644
index 3d85f7b3436479c2a59d415cb69ebac6b55d7897..0000000000000000000000000000000000000000
--- a/core/modules/system/lib/Drupal/system/Tests/Form/HTMLIdTest.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\system\Tests\Form\HTMLIdTest.
- */
-
-namespace Drupal\system\Tests\Form;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests uniqueness of generated HTML IDs.
- */
-class HTMLIdTest extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('form_test');
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Unique HTML IDs',
-      'description' => 'Tests functionality of drupal_html_id().',
-      'group' => 'Form API',
-    );
-  }
-
-  /**
-   * Tests that HTML IDs do not get duplicated when form validation fails.
-   */
-  function testHTMLId() {
-    $this->drupalGet('form-test/double-form');
-    $this->assertNoDuplicateIds('There are no duplicate IDs');
-
-    // Submit second form with empty title.
-    $edit = array();
-    $this->drupalPostForm(NULL, $edit, 'Save', array(), array(), 'form-test-html-id--2');
-    $this->assertNoDuplicateIds('There are no duplicate IDs');
-  }
-
-}
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index a9328686619ba0eb15c97ae6a67b2ff3877bc9e1..ecdbc7634f014dd6a4a525a6f81d6cf64dcbba51 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -2056,34 +2056,6 @@ function form_test_required_attribute($form, &$form_state) {
   return $form;
 }
 
-/**
- * Menu callback returns two instances of the same form.
- *
- * @deprecated \Drupal\form_test\Controller\FormTestController::doubleForm()
- */
-function form_test_double_form() {
-  return array(
-    'form1' => drupal_get_form('form_test_html_id'),
-    'form2' => drupal_get_form('form_test_html_id'),
-  );
-}
-
-/**
- * Builds a simple form to test duplicate HTML IDs.
- */
-function form_test_html_id($form, &$form_state) {
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => 'name',
-    '#required' => TRUE,
-  );
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => 'Save',
-  );
-  return $form;
-}
-
 /**
  * Builds a simple form to test form button classes.
  *
diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml
index de1ddea72bc52d04b46c34a9e5eb4976aaf8ca5c..a37c9d8cb772f6b1dc0249f51a51b27fc1e91926 100644
--- a/core/modules/system/tests/modules/form_test/form_test.routing.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml
@@ -77,14 +77,6 @@ form_test.wrapper:
   requirements:
     _access: 'TRUE'
 
-form_test.double_form:
-  path: '/form-test/double-form'
-  defaults:
-    _title: 'Double form test'
-    _content: '\Drupal\form_test\Controller\FormTestController::doubleForm'
-  requirements:
-    _access: 'TRUE'
-
 form_test.alter_form:
   path: '/form-test/alter'
   defaults:
diff --git a/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php b/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
index 40dd178ebcb0b2ba1fcaf3b6eab1e9f96e0c3291..e89718448975253a0fd2a8d8e4f509ab1342191c 100644
--- a/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
+++ b/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
@@ -42,11 +42,4 @@ public function wrapperCallback($form_id) {
     return form_test_wrapper_callback($form_id);
   }
 
-  /**
-   * @todo Remove form_test_double_form().
-   */
-  public function doubleForm() {
-    return form_test_double_form();
-  }
-
 }
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 777920abdd576476e7572b4b57c9e67abe2cba28..0b52a75aeb5088c49ead0f18c977000551e060e6 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -8,68 +8,17 @@
 namespace Drupal\Tests\Core\Form {
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Form\FormBuilder;
 use Drupal\Core\Form\FormInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Tests the form builder.
+ *
+ * @group Drupal
+ * @group Form
  */
-class FormBuilderTest extends UnitTestCase {
-
-  /**
-   * The form builder being tested.
-   *
-   * @var \Drupal\Core\Form\FormBuilder
-   */
-  protected $formBuilder;
-
-  /**
-   * The mocked URL generator.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Routing\UrlGeneratorInterface
-   */
-  protected $urlGenerator;
-
-  /**
-   * The mocked module handler.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * The expirable key value store used by form cache.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
-   */
-  protected $formCache;
-
-  /**
-   * The expirable key value store used by form state cache.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
-   */
-  protected $formStateCache;
-
-  /**
-   * The current user.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Session\AccountInterface
-   */
-  protected $account;
-
-  /**
-   * The CSRF token generator.
-   *
-   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Access\CsrfTokenGenerator
-   */
-  protected $csrfToken;
+class FormBuilderTest extends FormTestBase {
 
   /**
    * {@inheritdoc}
@@ -82,39 +31,6 @@ public static function getInfo() {
     );
   }
 
-  public function setUp() {
-    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-
-    $this->formCache = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
-    $this->formStateCache = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
-    $key_value_expirable_factory = $this->getMockBuilder('\Drupal\Core\KeyValueStore\KeyValueExpirableFactory')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $key_value_expirable_factory->expects($this->any())
-      ->method('get')
-      ->will($this->returnValueMap(array(
-        array('form', $this->formCache),
-        array('form_state', $this->formStateCache),
-      )));
-
-    $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
-    $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
-    $translation_manager = $this->getStringTranslationStub();
-    $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $http_kernel = $this->getMockBuilder('Drupal\Core\HttpKernel')
-      ->disableOriginalConstructor()
-      ->getMock();
-
-    $this->formBuilder = new TestFormBuilder($this->moduleHandler, $key_value_expirable_factory, $event_dispatcher, $this->urlGenerator, $translation_manager, $this->csrfToken, $http_kernel);
-    $this->formBuilder->setRequest(new Request());
-
-    $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $this->formBuilder->setCurrentUser($this->account);
-
-  }
-
   /**
    * Tests the getFormId() method with a string based form ID.
    */
@@ -729,147 +645,6 @@ public function testSendResponse() {
     $this->formBuilder->buildForm($form_arg, $form_state);
   }
 
-  /**
-   * Provides a mocked form object.
-   *
-   * @param string $form_id
-   *   (optional) The form ID to be used. If none is provided, the form will be
-   *   set with no expectation about getFormId().
-   * @param mixed $expected_form
-   *   (optional) If provided, the expected form response for buildForm() to
-   *   return. Defaults to NULL.
-   * @param int $count
-   *   (optional) The number of times the form is expected to be built. Defaults
-   *   to 1.
-   *
-   * @return \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Form\FormInterface
-   *   The mocked form object.
-   */
-  protected function getMockForm($form_id, $expected_form = NULL, $count = 1) {
-    $form = $this->getMock('Drupal\Core\Form\FormInterface');
-    $form->expects($this->once())
-      ->method('getFormId')
-      ->will($this->returnValue($form_id));
-
-    if ($expected_form) {
-      $form->expects($this->exactly($count))
-        ->method('buildForm')
-        ->will($this->returnValue($expected_form));
-    }
-    return $form;
-  }
-
-  /**
-   * Asserts that the expected form structure is found in a form for a given key.
-   *
-   * @param array $expected_form
-   *   The expected form structure.
-   * @param array $actual_form
-   *   The actual form.
-   * @param string|null $form_key
-   *   (optional) The form key to look in. Otherwise the entire form will be
-   *   compared.
-   */
-  protected function assertFormElement(array $expected_form, array $actual_form, $form_key = NULL) {
-    $expected_element = $form_key ? $expected_form[$form_key] : $expected_form;
-    $actual_element = $form_key ? $actual_form[$form_key] : $actual_form;
-    $this->assertSame(array_intersect_key($expected_element, $actual_element), $expected_element);
-  }
-
-}
-
-/**
- * Provides a test form builder class.
- */
-class TestFormBuilder extends FormBuilder {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function sendResponse(Response $response) {
-    parent::sendResponse($response);
-    // Throw an exception instead of exiting.
-    throw new \Exception('exit');
-  }
-
-  /**
-   * @param \Drupal\Core\Session\AccountInterface $account
-   */
-  public function setCurrentUser(AccountInterface $account) {
-    $this->currentUser = $account;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getElementInfo($type) {
-    $types['token'] = array(
-      '#input' => TRUE,
-    );
-    $types['value'] = array(
-      '#input' => TRUE,
-    );
-    $types['select'] = array(
-      '#input' => TRUE,
-      '#multiple' => FALSE,
-      '#empty_value' => '',
-    );
-    $types['radios'] = array(
-      '#input' => TRUE,
-    );
-    $types['textfield'] = array(
-      '#input' => TRUE,
-    );
-    $types['submit'] = array(
-      '#input' => TRUE,
-      '#name' => 'op',
-      '#is_button' => TRUE,
-    );
-    if (!isset($types[$type])) {
-      $types[$type] = array();
-    }
-    return $types[$type];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalInstallationAttempted() {
-    return FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalHtmlClass($class) {
-    return $class;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalHtmlId($id) {
-    return $id;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalStaticReset($name = NULL) {
-  }
-
 }
 
 class TestForm implements FormInterface {
@@ -892,48 +667,9 @@ public static function create(ContainerInterface $container) {
 }
 
 namespace {
-  function test_form_id() {
-    $form['test'] = array(
-      '#type' => 'textfield',
-      '#title' => 'Test',
-    );
-    $form['select'] = array(
-      '#type' => 'select',
-      '#options' => array(
-        'foo' => 'foo',
-        'bar' => 'bar',
-      ),
-    );
-    $form['options'] = array(
-      '#type' => 'radios',
-      '#options' => array(
-        'foo' => 'foo',
-        'bar' => 'bar',
-      ),
-    );
-    $form['value'] = array(
-      '#type' => 'value',
-      '#value' => 'bananas',
-    );
-    $form['actions'] = array(
-      '#type' => 'actions',
-    );
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => 'Submit',
-    );
-    return $form;
-  }
   function test_form_id_custom_submit(array &$form, array &$form_state) {
   }
-
   if (!defined('WATCHDOG_ERROR')) {
     define('WATCHDOG_ERROR', 3);
   }
-  if (!function_exists('batch_get')) {
-    function &batch_get() {
-      $batch = array();
-      return $batch;
-    }
-  }
 }
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b7a97f410b1a2e29fb0cc721ef9e692f1d66b2a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -0,0 +1,366 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormTestBase.
+ */
+
+namespace Drupal\Tests\Core\Form {
+
+use Drupal\Core\Form\FormBuilder;
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Provides a base class for testing form functionality.
+ *
+ * @see \Drupal\Core\Form\FormBuilder
+ */
+abstract class FormTestBase extends UnitTestCase {
+
+  /**
+   * The form builder being tested.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * The mocked URL generator.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * The mocked module handler.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The expirable key value store used by form cache.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
+   */
+  protected $formCache;
+
+  /**
+   * The expirable key value store used by form state cache.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
+   */
+  protected $formStateCache;
+
+  /**
+   * The current user.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The CSRF token generator.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfToken;
+
+  /**
+   * The request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The expirable key value factory.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\KeyValueStore\KeyValueExpirableFactory
+   */
+  protected $keyValueExpirableFactory;
+
+  /**
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $translationManager;
+
+  /**
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\HttpKernel
+   */
+  protected $httpKernel;
+
+  public function setUp() {
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+
+    $this->formCache = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
+    $this->formStateCache = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
+    $this->keyValueExpirableFactory = $this->getMockBuilder('Drupal\Core\KeyValueStore\KeyValueExpirableFactory')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->keyValueExpirableFactory->expects($this->any())
+      ->method('get')
+      ->will($this->returnValueMap(array(
+        array('form', $this->formCache),
+        array('form_state', $this->formStateCache),
+      )));
+
+    $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+    $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
+    $this->translationManager = $this->getStringTranslationStub();
+    $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->httpKernel = $this->getMockBuilder('Drupal\Core\HttpKernel')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->request = new Request();
+    $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
+
+    $this->setupFormBuilder();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function tearDown() {
+    $this->formBuilder->drupalStaticReset();
+  }
+
+  /**
+   * Sets up a new form builder object to test.
+   */
+  protected function setupFormBuilder() {
+    $this->formBuilder = new TestFormBuilder($this->moduleHandler, $this->keyValueExpirableFactory, $this->eventDispatcher, $this->urlGenerator, $this->translationManager, $this->csrfToken, $this->httpKernel);
+    $this->formBuilder->setRequest($this->request);
+    $this->formBuilder->setCurrentUser($this->account);
+  }
+
+  /**
+   * Provides a mocked form object.
+   *
+   * @param string $form_id
+   *   (optional) The form ID to be used. If none is provided, the form will be
+   *   set with no expectation about getFormId().
+   * @param mixed $expected_form
+   *   (optional) If provided, the expected form response for buildForm() to
+   *   return. Defaults to NULL.
+   * @param int $count
+   *   (optional) The number of times the form is expected to be built. Defaults
+   *   to 1.
+   *
+   * @return \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Form\FormInterface
+   *   The mocked form object.
+   */
+  protected function getMockForm($form_id, $expected_form = NULL, $count = 1) {
+    $form = $this->getMock('Drupal\Core\Form\FormInterface');
+    $form->expects($this->once())
+      ->method('getFormId')
+      ->will($this->returnValue($form_id));
+
+    if ($expected_form) {
+      $form->expects($this->exactly($count))
+        ->method('buildForm')
+        ->will($this->returnValue($expected_form));
+    }
+    return $form;
+  }
+
+  /**
+   * Simulates a form submission within a request, bypassing submitForm().
+   *
+   * Calling submitForm() will reset the form builder, if two forms were on the
+   * same page, they will be submitted simultaneously.
+   *
+   * @param string $form_id
+   *   The unique string identifying the form.
+   * @param \Drupal\Core\Form\FormInterface $form_arg
+   *   The form object.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return array
+   *   The built form.
+   */
+  protected function simulateFormSubmission($form_id, FormInterface $form_arg, array &$form_state) {
+    $form_state['build_info']['callback_object'] = $form_arg;
+    $form_state['build_info']['args'] = array();
+    $form_state['input']['op'] = 'Submit';
+    $form_state['programmed'] = TRUE;
+    $form_state['submitted'] = TRUE;
+    return $this->formBuilder->buildForm($form_id, $form_state);
+  }
+
+  /**
+   * Asserts that the expected form structure is found in a form for a given key.
+   *
+   * @param array $expected_form
+   *   The expected form structure.
+   * @param array $actual_form
+   *   The actual form.
+   * @param string|null $form_key
+   *   (optional) The form key to look in. Otherwise the entire form will be
+   *   compared.
+   */
+  protected function assertFormElement(array $expected_form, array $actual_form, $form_key = NULL) {
+    $expected_element = $form_key ? $expected_form[$form_key] : $expected_form;
+    $actual_element = $form_key ? $actual_form[$form_key] : $actual_form;
+    $this->assertSame(array_intersect_key($expected_element, $actual_element), $expected_element);
+  }
+
+}
+
+/**
+ * Provides a test form builder class.
+ */
+class TestFormBuilder extends FormBuilder {
+  protected static $seenIds = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function sendResponse(Response $response) {
+    parent::sendResponse($response);
+    // Throw an exception instead of exiting.
+    throw new \Exception('exit');
+  }
+
+  /**
+   * @param \Drupal\Core\Session\AccountInterface $account
+   */
+  public function setCurrentUser(AccountInterface $account) {
+    $this->currentUser = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getElementInfo($type) {
+    $types['token'] = array(
+      '#input' => TRUE,
+    );
+    $types['value'] = array(
+      '#input' => TRUE,
+    );
+    $types['radios'] = array(
+      '#input' => TRUE,
+    );
+    $types['textfield'] = array(
+      '#input' => TRUE,
+    );
+    $types['submit'] = array(
+      '#input' => TRUE,
+      '#name' => 'op',
+      '#is_button' => TRUE,
+    );
+    if (!isset($types[$type])) {
+      $types[$type] = array();
+    }
+    return $types[$type];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalInstallationAttempted() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function menuGetItem() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalHtmlClass($class) {
+    return $class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalHtmlId($id) {
+    if (isset(static::$seenIds[$id])) {
+      $id = $id . '--' . ++static::$seenIds[$id];
+    }
+    else {
+      static::$seenIds[$id] = 1;
+    }
+    return $id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function drupalStaticReset($name = NULL) {
+    static::$seenIds = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function &batchGet() {
+    $batch = array();
+    return $batch;
+  }
+
+}
+
+}
+
+namespace {
+
+  function test_form_id() {
+    $form['test'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Test',
+    );
+    $form['options'] = array(
+      '#type' => 'radios',
+      '#options' => array(
+        'foo' => 'foo',
+        'bar' => 'bar',
+      ),
+    );
+    $form['value'] = array(
+      '#type' => 'value',
+      '#value' => 'bananas',
+    );
+    $form['actions'] = array(
+      '#type' => 'actions',
+    );
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => 'Submit',
+    );
+    return $form;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidationTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..20edd0660c859f86c96b4c83bf5edd4e4bcab656
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormValidationTest.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormValidationTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+/**
+ * Tests various form element validation mechanisms.
+ *
+ * @group Drupal
+ * @group Form
+ */
+class FormValidationTest extends FormTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Form element validation',
+      'description' => 'Tests various form element validation mechanisms.',
+      'group' => 'Form API',
+    );
+  }
+
+  public function testNoDuplicateErrorsForIdenticalForm() {
+    $form_id = 'test_form_id';
+    $expected_form = $form_id();
+    $expected_form['test']['#required'] = TRUE;
+
+    // Mock a form object that will be built three times.
+    $form_arg = $this->getMockForm($form_id, $expected_form, 3);
+
+    // The first form will have errors.
+    $form_state = array();
+    $this->formBuilder->getFormId($form_arg, $form_state);
+    $this->simulateFormSubmission($form_id, $form_arg, $form_state);
+    $errors = $this->formBuilder->getErrors($form_state);
+    $this->assertNotEmpty($errors['test']);
+
+    // The second form will not have errors.
+    $form_state = array();
+    $this->simulateFormSubmission($form_id, $form_arg, $form_state);
+    $errors = $this->formBuilder->getErrors($form_state);
+    $this->assertEmpty($errors);
+
+    // Reset the form builder.
+    $this->setupFormBuilder();
+
+    // On a new request, the first form will have errors again.
+    $form_state = array();
+    $this->simulateFormSubmission($form_id, $form_arg, $form_state);
+    $errors = $this->formBuilder->getErrors($form_state);
+    $this->assertNotEmpty($errors['test']);
+  }
+
+  public function testUniqueHtmlId() {
+    $form_id = 'test_form_id';
+    $expected_form = $form_id();
+    $expected_form['test']['#required'] = TRUE;
+
+    // Mock a form object that will be built three times.
+    $form_arg = $this->getMockForm($form_id, $expected_form, 2);
+
+    $form_state = array();
+    $this->formBuilder->getFormId($form_arg, $form_state);
+    $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
+    $this->assertSame($form_id, $form['#id']);
+
+    $form_state = array();
+    $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
+    $this->assertSame("$form_id--2", $form['#id']);
+  }
+
+}