Functional Testing Drupal Forms with BrowserTestBasefor Drupal 8 , 9 , 10 , and 11

Last updated :  

A unit test proves your validation logic is correct in isolation. It says nothing about whether the field actually renders, whether clicking Submit actually posts the form, or whether the confirmation message actually appears on the resulting page. For that, you need a test that behaves like a real visitor — that's exactly what BrowserTestBase gives you.

What you'll learn in this lesson

  • What makes BrowserTestBase fundamentally different from UnitTestCase
  • How to spin up a real (if minimal) Drupal install for the duration of one test
  • The core assertion helpers: drupalGet(), assertSession(), statusCodeEquals()
  • Why these are slower than unit tests, and when that tradeoff is worth it

A real functional test, from the Drupal Examples module

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;

/**
 * Test the user-facing menus in PHPUnit Example.
 *
 * @group phpunit_example
 * @group examples
 */
class PHPUnitExampleMenuTest extends BrowserTestBase {

  protected $defaultTheme = 'stark';

  protected static $modules = ['phpunit_example'];

  protected $profile = 'minimal';

  public function testLinksAndPages() {
    $this->drupalLogin($this->createUser(['access content']));
    $assert = $this->assertSession();
    $links = [
      '' => Url::fromRoute('phpunit_example.description'),
    ];
    foreach ($links as $page => $path) {
      $this->drupalGet($page);
      $assert->linkByHrefExists($path->getInternalPath());
    }
    foreach ($links as $path) {
      $this->drupalGet($path);
      $assert->statusCodeEquals(200);
    }
  }

}

A full Drupal install, spun up just for this test

protected static $modules = ['phpunit_example'];
protected $profile = 'minimal';

Unlike a unit test, BrowserTestBase actually installs a throwaway Drupal site in a SQLite (or configured test) database before your test method runs — with only the modules you list in $modules enabled, on top of the given install profile. That's why these tests are dramatically slower than unit tests: every single test method gets its own fresh install.

drupalGet() and assertSession() — the real browser simulation

$this->drupalLogin($this->createUser(['access content']));
$this->drupalGet($page);
$assert->linkByHrefExists($path->getInternalPath());
$assert->statusCodeEquals(200);

createUser() and drupalLogin() create and authenticate as a real user with exactly the permissions you specify — testing access control is a first-class concern here, not an afterthought. drupalGet() performs a real HTTP request through Drupal's full page-rendering pipeline. assertSession() returns a fluent assertion object with dozens of methods (pageTextContains(), fieldExists(), buttonExists(), statusCodeEquals()) for checking exactly what came back.

For actually submitting a form, the workhorse method is $this->submitForm($edit, 'Submit') — pass an array of field names to values (matching each element's #name, not its #title) and the button's label. It fills the fields and clicks the button in one call, exactly like a real visitor would.

Quick check: why does this test call createUser(['access content']) instead of just visiting the page as an anonymous user? (Because the test is deliberately verifying access-controlled behavior for an authenticated user with a specific, minimal permission set — testing "does this work for someone who's logged in with only this one permission" is a different, equally important question from "does this work for anyone at all.")

Key takeaways

  • BrowserTestBase installs a real, throwaway Drupal site per test method — genuinely functional, genuinely slow compared to unit tests.
  • drupalGet() performs a real request through Drupal's full render pipeline; assertSession() gives you a large, fluent set of assertion helpers against the resulting page.
  • Use $this->submitForm($edit, 'Submit') to fill and submit a form exactly like a real visitor — the array keys must match each element's #name.
  • createUser()/drupalLogin() let you test as a specific, permission-scoped user rather than always testing as anonymous or as an admin.
  • Reach for BrowserTestBase when you need to verify end-to-end behavior — rendering, access control, full-page interaction — not for testing pure logic, where a unit test is faster and simpler.

Coming up next

You now have both ends of the testing spectrum: fast, isolated unit tests and slow, realistic functional tests. Next, we zoom in specifically on testing form validation logic — writing tests that deliberately submit bad data and assert that your form correctly rejects it.