Testing Route Model Binding in Laravel

Testing Route Model Binding in Laravel

In Laravel, route model binding allows you to automatically inject model instances into your routes. This post focuses on how to write tests for routes using model binding, including edge cases like invalid slugs and soft-deleted models.

Step 1: Define the Problem (Testing Route Model Binding)

Imagine you're working on a blog application that fetches posts by their unique slugs instead of IDs. You need to:

  • Create a route that uses route model binding to fetch the Post model by slug.
  • Test that the route injects the correct model instance into the controller.
  • Handle missing posts gracefully by showing a 404 error page.

Step 2: Setting Up Route Model Binding

To begin, define a route in your routes/web.php file:


use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/post/{post:slug}', function (Post $post) {
    return view('post.show', compact('post'));
})->name('post.show');

Here, the {post:slug} syntax binds the Post model by its slug column instead of the default id.

Step 3: Create a Controller for the Route

Now let's create a controller to handle this logic:


php artisan make:controller PostController

Modify the PostController.php to handle displaying the post:


namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function show(Post $post)
    {
        return view('post.show', compact('post'));
    }
}

Step 4: Write Tests for Route Model Binding

Next, write tests to ensure that the route model binding works as expected. Create a test class:


php artisan make:test PostRouteModelBindingTest

Edit the generated test file:


namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostRouteModelBindingTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_displays_a_post_when_a_valid_slug_is_provided()
    {
        // Arrange
        $post = Post::factory()->create(['slug' => 'my-first-post']);

        // Act
        $response = $this->get(route('post.show', $post->slug));

        // Assert
        $response->assertStatus(200);
        $response->assertViewIs('post.show');
        $response->assertSee($post->title);
    }

    /** @test */
    public function it_returns_404_when_an_invalid_slug_is_provided()
    {
        // Act
        $response = $this->get(route('post.show', 'invalid-slug'));

        // Assert
        $response->assertStatus(404);
    }
}

Step 5: Testing Non-Existent Models

When a model is not found, Laravel throws a ModelNotFoundException. Customize this behavior to show a custom 404 page:


public function render($request, Throwable $exception)
{
    if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
        return response()->view('errors.404', [], 404);
    }

    return parent::render($request, $exception);
}

Step 6: Testing Edge Cases

To ensure that slugs with special characters are handled correctly, add the following test:


/** @test */
public function it_handles_special_characters_in_slug()
{
    $post = Post::factory()->create(['slug' => 'post-with-special-characters-!@#$']);

    $response = $this->get(route('post.show', $post->slug));

    $response->assertStatus(200);
    $response->assertSee($post->title);
}

Step 7: Handling Soft-Deleted Models

If a post is soft-deleted, Laravel will return a 404 page by default. You can ensure this behavior by adding the following test:


/** @test */
public function it_returns_404_for_soft_deleted_post()
{
    $post = Post::factory()->create();
    $post->delete();

    $response = $this->get(route('post.show', $post->slug));

    $response->assertStatus(404);
}

Conclusion

In this post, you’ve learned how to test route model binding in Laravel, handling edge cases like missing models, special characters, and soft-deleted models. These tests ensure your application behaves correctly when using route model binding.