A validation rule you've never tested with bad input is a validation rule you don't actually know works. It's easy to write a test that submits perfect data and watch it pass — the real value comes from deliberately submitting the data your validation is supposed to reject, and proving it actually gets rejected.
What you'll learn in this lesson
- How to submit deliberately invalid data with
submitForm() - How to assert that a specific validation error message actually appeared
- How to assert the form did NOT proceed past validation
- The difference between testing at the unit level and the functional level for the same validation rule
Submitting bad data on purpose
public function testInvalidEmailRejected() {
$this->drupalGet('/my-form-route');
$edit = [
'name' => 'Test User',
'email' => 'not-a-valid-email',
];
$this->submitForm($edit, 'Submit');
$assert = $this->assertSession();
$assert->pageTextContains('The email address is not valid.');
$assert->addressEquals('/my-form-route');
}
The shape of this test is deliberately the mirror image of a "happy path" test: instead of filling in correct values and checking for a success message, it fills in a value your validation logic is specifically supposed to catch, then asserts two things — that the expected error text actually rendered, and that the browser is still sitting on the form page rather than having moved on to a redirect target.
pageTextContains() vs. checking for the error programmatically
Asserting on the rendered error text (rather than, say, inspecting $form_state directly) is intentional: it proves the error message a real visitor would actually see is correct, not just that validation logic internally flagged something. A validation rule that sets an error the user never sees rendered is arguably not fixed at all from the user's perspective.
addressEquals() — proving the form didn't silently proceed
$assert->addressEquals('/my-form-route');
This is easy to skip and important not to: a bug where validation correctly shows an error message but the form processes the submission anyway is a real, dangerous class of bug. Asserting the URL didn't change confirms Drupal actually stopped the submission at the validation step, not just that it printed a warning alongside a submission that went through regardless.
Quick check: your test submits valid data but the assertion `$assert->addressEquals('/success-page')` still fails, even though `pageTextContains('Thank you')` passed. What's the most likely explanation? (The confirmation message is rendering on the form page itself rather than after a redirect — some forms show a status message via the Messenger service without calling a redirect at all. That's not necessarily a bug, but the test's assumption about the URL needs to match what the form actually does.)
Key takeaways
- Test validation by deliberately submitting the exact data your validation logic is supposed to reject, not just data that should pass.
- Assert both that the expected error message rendered (
pageTextContains()) and that the form didn't silently proceed (addressEquals()the original page). - A functional-level validation test is complementary to a unit test of the same rule — the unit test proves the logic, the functional test proves it's correctly wired to the real form and visible to real users.
- Cover every distinct invalid-input case your validation handles with its own test method or data provider row, not just one generic "bad data" test.
Coming up next
You've tested that bad data gets rejected — next we test the other half: that valid submissions actually do what they're supposed to, whether that's saving an entity, updating config, or triggering a custom action.