Laravel Testing Strategies: Building Confidence in Your Code
Why Testing Matters
Tests aren't about proving your code works—they're about enabling change. With a comprehensive test suite, you can refactor with confidence, upgrade dependencies without fear, and deploy on Friday afternoon (though I still don't recommend it). Tests are documentation that actually stays up to date.
The Testing Pyramid
A healthy test suite follows the testing pyramid: many unit tests at the base, fewer integration tests in the middle, and a small number of end-to-end tests at the top. Each level has its purpose.
Unit Tests
Unit tests verify individual components in isolation. They're fast, focused, and should make up the majority of your test suite:
class MoneyTest extends TestCase
{
public function test_can_add_money_with_same_currency(): void
{
$a = new Money(100, 'USD');
$b = new Money(50, 'USD');
$result = $a->add($b);
$this->assertEquals(150, $result->amount);
$this->assertEquals('USD', $result->currency);
}
public function test_cannot_add_money_with_different_currencies(): void
{
$this->expectException(CurrencyMismatchException::class);
$usd = new Money(100, 'USD');
$eur = new Money(50, 'EUR');
$usd->add($eur);
}
}
Feature Tests
Feature tests verify that components work together correctly. In Laravel, these typically test HTTP endpoints:
class OrderCreationTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_create_order(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 99.99]);
$response = $this->actingAs($user)->post('/orders', [
'product_id' => $product->id,
'quantity' => 2,
]);
$response->assertRedirect('/orders');
$this->assertDatabaseHas('orders', [
'user_id' => $user->id,
'total' => 199.98,
]);
}
public function test_guest_cannot_create_order(): void
{
$product = Product::factory()->create();
$response = $this->post('/orders', [
'product_id' => $product->id,
'quantity' => 1,
]);
$response->assertRedirect('/login');
}
}
Database Testing Strategies
Use RefreshDatabase for test isolation. Use factories to create test data expressively:
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'status' => 'pending',
'total' => fake()->randomFloat(2, 10, 1000),
];
}
public function completed(): static
{
return $this->state([
'status' => 'completed',
'completed_at' => now(),
]);
}
}
// Usage
$completedOrders = Order::factory()->count(5)->completed()->create();
Mocking External Services
Don't let tests depend on external services. Mock them:
public function test_order_sends_confirmation_email(): void
{
Mail::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
(new OrderConfirmationJob($order))->handle();
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
public function test_handles_payment_gateway_failure(): void
{
$this->mock(PaymentGateway::class)
->shouldReceive('charge')
->andThrow(new PaymentFailedException('Card declined'));
$response = $this->actingAs($this->user)
->post('/checkout', $this->validCheckoutData);
$response->assertSessionHasErrors(['payment']);
}
Browser Testing with Dusk
For critical user journeys, browser tests catch issues that other tests miss:
public function test_user_can_complete_checkout(): void
{
$this->browse(function (Browser $browser) {
$browser->loginAs($this->user)
->visit('/products/1')
->press('Add to Cart')
->visit('/cart')
->press('Checkout')
->type('card_number', '4242424242424242')
->press('Pay Now')
->assertPathIs('/orders/confirmation')
->assertSee('Thank you for your order');
});
}
Continuous Integration
Automate your tests with CI. Every pull request should run the full test suite before merging.
Conclusion
Testing isn't optional for professional software development. Start with the testing pyramid, use factories for expressive test data, mock external services, and automate everything with CI. Your future self will thank you.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch