Testing Laravel Apps
๐งช Testing Laravel Apps
TestCase; Laravel's equivalent is a trait you have to remember to add.
Feature Tests vs Unit Tests
Laravel draws an explicit line Django doesn't โ tests/Feature/ exercises the full stack through an HTTP request, tests/Unit/ tests a single class in isolation with no framework bootstrapping at all:
Feature Test
Simulates a real HTTP request through routing, middleware, controller, and response โ Django's self.client.get()-style tests fill this same role.
Unit Test
Tests one class or method directly โ a plain PHPUnit test with no Laravel application booted, useful for a pure calculation or validator method.
๐ RefreshDatabase: Opt-In, Not Automatic
This is the sharpest difference from Django in this whole course. Django's TestCase wraps every test in a rollback transaction automatically, just by inheriting it. Laravel requires an explicit trait:
// tests/Feature/BookTest.php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class BookTest extends TestCase { use RefreshDatabase; // โ ๏ธ without this line, tests share a real, un-reset database public function test_book_list_returns_200(): void { $response = $this->get('/books'); $response->assertStatus(200); } }
Automatic (Django) vs Opt-In (Laravel)
Django
class BookTests(TestCase): โ inheriting TestCase is the whole story. Every test method is automatically wrapped in a rollback transaction; there's no separate step to remember.
Laravel
use RefreshDatabase; must be added explicitly inside the class โ extending Tests\TestCase alone does not reset the database between tests.
The HTTP Testing API
Laravel's fluent testing methods read close to Django's self.client, with more built-in assertion helpers:
$response = $this->get('/books'); $response->assertStatus(200); $response->assertSee('The Dispossessed'); $response->assertViewIs('books.index'); // Testing an authenticated route (Chapter 1's auth middleware) $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); // Testing a JSON API endpoint (Chapter 2's API Resources) $response = $this->getJson('/api/books'); $response->assertJson(['data' => []]);
actingAs($user)
Simulates a logged-in session for the test โ the direct equivalent of manually calling self.client.login() in Django, just one method instead of two steps.
assertViewIs()
Confirms which Blade view actually rendered โ the same job as Django's assertTemplateUsed.
๐ญ Model Factories
A genuinely nice built-in feature Django doesn't have a core equivalent for โ Django developers commonly reach for a third-party library like factory_boy; Laravel ships factories as part of the framework itself:
php artisan make:factory BookFactory
// database/factories/BookFactory.php namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; class BookFactory extends Factory { public function definition(): array { return [ 'title' => fake()->sentence(3), 'price' => fake()->randomFloat(2, 5, 50), ]; } }
// In a test: $book = Book::factory()->create(); // one realistic, saved Book $books = Book::factory()->count(10)->create(); // ten of them $book = Book::factory()->make(['price' => 0]); // override one field, don't save
๐ป Coding Challenges
Challenge 1: Write a Feature Test
Write a feature test class for the book detail route that uses RefreshDatabase, creates a book with a factory, and asserts the response is a 200 containing the book's title.
Goal: Practice the full RefreshDatabase + factory + HTTP assertion workflow.
Challenge 2: Test an Authenticated Route
Write a test using actingAs() to confirm a logged-in user can reach /dashboard, and a second test confirming an anonymous request is redirected instead.
Goal: Practice testing both the success and the access-control-denied path for a protected route.
Challenge 3: Diagnose a Test Suite Missing RefreshDatabase
Given a test class that extends Tests\TestCase but omits use RefreshDatabase;, and two tests that each create a Book and assert Book::count() equals 1 โ explain why the second test might fail when run after the first, and fix it.
Goal: Practice recognizing test pollution caused by the missing trait, and understand exactly why it happens.
RefreshDatabaseThis is the sharpest version of a pattern this project has built toward โ Django simply doesn't let you make this mistake, because the rollback is baked into TestCase itself. In Laravel, a test class that extends Tests\TestCase but omits use RefreshDatabase; runs every test against the same real database, with no cleanup between tests at all. Data created in one test persists into the next, test order starts mattering, and a suite that passes in isolation can fail unpredictably when run as a whole โ classic flaky-test symptoms, all traceable to one missing line. Add the trait to every feature test that touches the database, as a default habit, not an afterthought.
๐ฏ What's Next
With a real test suite in place, the next chapter looks at code that reacts to events rather than sitting inline in a controller: Events & Listeners โ Laravel's decoupled event system compared to Django's signals.