Skip to content

Is your validation test passing for the wrong reason?

Aug 11, 2026 2 min read #laravel #testing

Originally published on Mastering Laravel.

A common pattern when testing Laravel validation looks like this:

$response->assertSessionHasErrors(['username']);
php

(Livewire has its own assertHasErrors that works nearly the same way, and everything below applies there too.)

This only says “the form rejected the input and there was an error on the username field.” That’s a weaker claim than what we actually want to prove, which is that a specific rule rejected this input.

Here’s where it falls apart. Imagine a test class covering registration, with a setUp() that seeds the users these tests typically need:

class RegistrationTest extends TestCase
{
  protected function setUp(): void
  {
    parent::setUp();

    User::factory()->create(['username' => 'admin']);
    User::factory()->create(['username' => 'regular-user']);
  }
}
php

Every test in the class starts with a known admin and a known regular user.

Now, let’s look at my contrived registration form with just two rules on username:

public function rules(): array
{
  return [
    'username' => [new NotReservedUsername, 'unique:users'],
  ];
}
php

NotReservedUsername is a custom rule that rejects admin, root, and system. unique:users is the standard duplicate check.

The test for the custom rule:

public function testCannotRegisterWithReservedUsername(): void
{
  $response = $this->post('/register', ['username' => 'admin']);

  $response->assertSessionHasErrors(['username']);
}
php

This passes. But what if later someone refactors NotReservedUsername into a no-op accidentally? The test still passes. admin already exists from setUp(), unique:users fires, and the field has an error. The assertion can’t tell which rule produced it and the broken custom rule slips by our tests.

The fix is to assert on the specific message:

$response->assertSessionHasErrors([
  'username' => 'This username is reserved.',
]);
php

Now if NotReservedUsername stops firing, the assertion fails, because unique:users produces a different message and won’t satisfy the check.

I do contract work - and specialize in Laravel. If you need a hand, let me know.