TeachyLeaf library

What are you looking for?

Search practical tutorials and browser-based tools.

Laravel · Testing · Beginner

Laravel feature testing: routes, forms, and database behavior

Use real TeachyLeaf tests to see how Laravel checks HTTP responses, contact-form behavior, email intent, and database-backed blog queries.

Who this is for: Laravel beginners who can read routes and controllers and want to check application behavior with automated tests.

What you will learn: how a feature test sends a request through Laravel, how to assert responses and validation errors, how to fake outgoing mail, and how to isolate database tests.

Prerequisites: Basic PHP and familiarity with Laravel routes and forms. The examples below are based on the tests in this project.

Tested environment: Laravel Framework 13.16.1 and PHP 8.4.15. PHPUnit is configured to use SQLite in memory, the array mailer, and a synchronous queue for tests. These settings are defined in phpunit.xml.

What does a feature test check?

A feature test makes a request through Laravel's application and checks the observable result: a successful page response, a redirect, a validation error, a rendered value, or a mail action. Unlike a unit test of one isolated method, it can cover several application layers together without running a production web server.

This project keeps these checks in tests/Feature/. Run them with:

php artisan test

Check a form's response and validation errors

TeachyLeaf's contact tests submit data to the real contact.store route. A valid submission is checked for its redirect and mail action; a separate test submits invalid values and checks that validation errors are present.

$response = $this->from('/contact')->post(route('contact.store'), [
    'name' => 'Amit Kumar Das',
    'email' => 'amit@example.com',
    'subject' => 'Tutorial correction',
    'message' => 'Please review this example.',
]);

$response->assertRedirect(route('contact'));

$invalidResponse = $this->from('/contact')->post(route('contact.store'), [
    'name' => 'A',
    'email' => 'not-an-email',
    'subject' => 'X',
    'message' => 'short',
]);

$invalidResponse->assertSessionHasErrors(['name', 'email', 'subject', 'message']);

The test does not send its invalid example to the live site. It runs inside the local Laravel test process and verifies the validation response that the application creates.

Fake external mail in automated tests

Automated tests should not send real messages. Mail::fake() replaces delivery with a test double, allowing the test to check that the application attempted to send the expected mailable:

Mail::fake();

$response = $this->post(route('contact.store'), [
    'name' => 'Amit Kumar Das',
    'email' => 'amit@example.com',
    'subject' => 'Tutorial correction',
    'message' => 'Please review this example.',
]);

$response->assertRedirect(route('contact'));
Mail::assertSent(ContactMessage::class);

Other contact tests verify that invalid input produces session errors, a filled honeypot does not send mail, and a simulated transport exception returns an error message. These are deterministic checks of application behavior; the fake mailer does not connect to SMTP or prove that an email arrives.

That is why automated and live checks serve different purposes. On 2026-11-09, the site owner manually confirmed that a production contact-form submission reached the intended inbox and the form worked as expected.

Use isolated database tests for blog behavior

The Eloquent lesson tests use Laravel's RefreshDatabase trait and Blog factories. The trait prepares a clean schema for the test; factories create records with controlled status, slugs, and dates so the test can check ordering and draft visibility.

use Illuminate\Foundation\Testing\RefreshDatabase;

class BlogPageTest extends TestCase
{
    use RefreshDatabase;

    public function test_blog_hides_drafts(): void
    {
        Blog::factory()->create([
            'title' => 'Visible published post',
            'slug' => 'visible-published-post',
            'status' => 'Published',
        ]);
        Blog::factory()->create([
            'title' => 'Private draft post',
            'slug' => 'private-draft-post',
            'status' => 'Draft',
        ]);

        $response = $this->get('/blog');

        $response->assertOk();
        $response->assertSee('Visible published post');
        $response->assertDontSee('Private draft post');
    }
}

Factories make test records explicit and repeatable. Assertions should check behavior that matters to a visitor rather than implementation details that may change without changing the result.

Know what the test environment does not prove

  • An in-memory SQLite test does not prove a schema change behaves identically on a production database engine.
  • A mail fake checks the application's intent to send, not DNS, SMTP credentials, delivery, spam filtering, or inbox placement.
  • An HTTP feature test does not by itself check the appearance of a page in every browser or at every screen size.
  • Passing tests cover only the cases they exercise; review edge cases and test failures rather than treating a green suite as a guarantee.

Use tests for fast, repeatable feedback, then use environment-appropriate staging, browser, and production checks for behavior the test configuration cannot observe.

Try it in this project

  1. Read tests/Feature/ContactPageTest.php and find the valid-submission, invalid-input, honeypot, and mail-failure tests.
  2. Read tests/Feature/LaravelEloquentBlogQueriesLessonTest.php and identify where factories create published and draft posts.
  3. Inspect the test environment in phpunit.xml, especially DB_DATABASE and MAIL_MAILER.
  4. Run php artisan test --compact and review any failing test output before changing code.

References

Continue learning

Follow the contact form validation lesson to understand the request being tested, then see how the blog query and slug route work. Before applying schema changes to a real database, read the migration lesson on inspecting and verifying migrations.