Testing in Laravel: Comprehensive Testing Strategies & Best Practices
Testing in Laravel: Comprehensive Testing Strategies & Best Practices
Expert Guide by Alaa Amer – Professional Web Developer & Applications Designer
Laravel provides robust testing infrastructure with PHPUnit integration. Master comprehensive testing strategies, TDD practices, and enterprise-level quality assurance techniques.
2️⃣ Comprehensive Unit Testing
Model Unit Tests:
<?php
// tests/Unit/Models/PostTest.php
namespace Tests\Unit\Models;
use Tests\TestCase;
use App\Models\{Post, User, Category, Tag};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Carbon\Carbon;
class PostTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->category = Category::factory()->create();
$this->post = Post::factory()->create([
'user_id' => $this->user->id,
'category_id' => $this->category->id
]);
}
/** @test */
public function it_belongs_to_a_user(): void
{
$this->assertInstanceOf(User::class, $this->post->user);
$this->assertEquals($this->user->id, $this->post->user->id);
}
/** @test */
public function it_belongs_to_a_category(): void
{
$this->assertInstanceOf(Category::class, $this->post->category);
$this->assertEquals($this->category->id, $this->post->category->id);
}
/** @test */
public function it_can_have_many_tags(): void
{
$tags = Tag::factory()->count(3)->create();
$this->post->tags()->attach($tags->pluck('id'));
$this->assertCount(3, $this->post->tags);
$this->assertInstanceOf(Tag::class, $this->post->tags->first());
}
/** @test */
public function it_can_check_if_published(): void
{
// Test published post
$publishedPost = Post::factory()->published()->create();
$this->assertTrue($publishedPost->isPublished());
// Test draft post
$draftPost = Post::factory()->draft()->create();
$this->assertFalse($draftPost->isPublished());
// Test scheduled post (future date)
$scheduledPost = Post::factory()->create([
'status' => 'scheduled',
'published_at' => now()->addDay()
]);
$this->assertFalse($scheduledPost->isPublished());
}
/** @test */
public function it_generates_slug_automatically(): void
{
$post = Post::factory()->make([
'title' => 'Laravel Testing Best Practices',
'slug' => null
]);
$post->save();
$this->assertEquals('laravel-testing-best-practices', $post->slug);
}
/** @test */
public function it_ensures_unique_slugs(): void
{
$firstPost = Post::factory()->create([
'title' => 'Laravel Testing',
'slug' => 'laravel-testing'
]);
$secondPost = Post::factory()->create([
'title' => 'Laravel Testing',
'slug' => null
]);
$this->assertEquals('laravel-testing', $firstPost->slug);
$this->assertEquals('laravel-testing-1', $secondPost->slug);
}
/** @test */
public function it_calculates_reading_time(): void
{
$shortContent = str_repeat('word ', 100); // ~100 words
$longContent = str_repeat('word ', 600); // ~600 words
$shortPost = Post::factory()->create(['content' => $shortContent]);
$longPost = Post::factory()->create(['content' => $longContent]);
$this->assertEquals(1, $shortPost->reading_time); // Min 1 minute
$this->assertEquals(3, $longPost->reading_time); // ~3 minutes
}
/** @test */
public function it_scopes_published_posts(): void
{
Post::factory()->published()->count(5)->create();
Post::factory()->draft()->count(3)->create();
Post::factory()->create([
'status' => 'scheduled',
'published_at' => now()->addDay()
]);
$publishedPosts = Post::published()->get();
$this->assertCount(5, $publishedPosts);
$publishedPosts->each(function ($post) {
$this->assertEquals('published', $post->status);
$this->assertLessThanOrEqual(now(), $post->published_at);
});
}
/** @test */
public function it_scopes_featured_posts(): void
{
Post::factory()->featured()->count(3)->create();
Post::factory()->count(5)->create(['is_featured' => false]);
$featuredPosts = Post::featured()->get();
$this->assertCount(3, $featuredPosts);
$featuredPosts->each(function ($post) {
$this->assertTrue($post->is_featured);
});
}
/** @test */
public function it_can_be_searched_by_content(): void
{
Post::factory()->create([
'title' => 'Laravel Testing Guide',
'content' => 'This is about PHP unit testing'
]);
Post::factory()->create([
'title' => 'Vue.js Components',
'content' => 'This covers JavaScript testing'
]);
$laravelPosts = Post::search('Laravel')->get();
$testingPosts = Post::search('testing')->get();
$this->assertCount(1, $laravelPosts);
$this->assertCount(2, $testingPosts);
}
}
Service Layer Unit Tests:
<?php
// tests/Unit/Services/PostServiceTest.php
namespace Tests\Unit\Services;
use Tests\TestCase;
use App\Services\PostService;
use App\Models\{Post, User, Category};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class PostServiceTest extends TestCase
{
use RefreshDatabase;
protected PostService $postService;
protected User $user;
protected Category $category;
protected function setUp(): void
{
parent::setUp();
$this->postService = app(PostService::class);
$this->user = User::factory()->create();
$this->category = Category::factory()->create();
Storage::fake('public');
}
/** @test */
public function it_creates_post_with_valid_data(): void
{
$postData = [
'title' => 'Test Post Title',
'content' => 'This is the post content that is long enough for validation.',
'excerpt' => 'Short excerpt',
'category_id' => $this->category->id,
'status' => 'draft'
];
$post = $this->postService->createPost($postData, $this->user);
$this->assertInstanceOf(Post::class, $post);
$this->assertEquals($postData['title'], $post->title);
$this->assertEquals('test-post-title', $post->slug);
$this->assertEquals($this->user->id, $post->user_id);
$this->assertDatabaseHas('posts', [
'title' => $postData['title'],
'user_id' => $this->user->id
]);
}
/** @test */
public function it_creates_post_with_image_upload(): void
{
$image = UploadedFile::fake()->image('test.jpg', 800, 600);
$postData = [
'title' => 'Post with Image',
'content' => 'Content with uploaded image.',
'category_id' => $this->category->id,
'featured_image' => $image
];
$post = $this->postService->createPost($postData, $this->user);
$this->assertNotNull($post->featured_image);
$this->assertStringContains('posts/', $post->featured_image);
Storage::disk('public')->assertExists($post->featured_image);
}
/** @test */
public function it_updates_post_data(): void
{
$post = Post::factory()->create([
'user_id' => $this->user->id,
'title' => 'Original Title',
'content' => 'Original content'
]);
$updateData = [
'title' => 'Updated Title',
'content' => 'Updated content with more information.'
];
$updatedPost = $this->postService->updatePost($post, $updateData);
$this->assertEquals('Updated Title', $updatedPost->title);
$this->assertEquals('updated-title', $updatedPost->slug);
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'title' => 'Updated Title'
]);
}
/** @test */
public function it_publishes_draft_post(): void
{
$post = Post::factory()->draft()->create(['user_id' => $this->user->id]);
$this->assertNull($post->published_at);
$this->assertEquals('draft', $post->status);
$publishedPost = $this->postService->publishPost($post);
$this->assertEquals('published', $publishedPost->status);
$this->assertNotNull($publishedPost->published_at);
$this->assertLessThanOrEqual(now(), $publishedPost->published_at);
}
/** @test */
public function it_schedules_post_for_future_publication(): void
{
$post = Post::factory()->draft()->create(['user_id' => $this->user->id]);
$futureDate = now()->addDays(3);
$scheduledPost = $this->postService->schedulePost($post, $futureDate);
$this->assertEquals('scheduled', $scheduledPost->status);
$this->assertEquals($futureDate->format('Y-m-d H:i:s'), $scheduledPost->published_at->format('Y-m-d H:i:s'));
}
/** @test */
public function it_deletes_post_with_dependencies(): void
{
$post = Post::factory()->create(['user_id' => $this->user->id]);
// Add some comments and likes
$post->comments()->create([
'content' => 'Test comment',
'user_id' => $this->user->id
]);
$this->assertTrue($this->postService->deletePost($post));
$this->assertModelSoftDeleted($post);
$this->assertDatabaseMissing('comments', [
'post_id' => $post->id,
'deleted_at' => null
]);
}
/** @test */
public function it_gets_related_posts(): void
{
$category = Category::factory()->create();
$mainPost = Post::factory()->published()->create([
'category_id' => $category->id
]);
// Create related posts in same category
$relatedPosts = Post::factory()->published()->count(5)->create([
'category_id' => $category->id
]);
// Create posts in different category
Post::factory()->published()->count(3)->create([
'category_id' => Category::factory()->create()->id
]);
$result = $this->postService->getRelatedPosts($mainPost, 3);
$this->assertCount(3, $result);
$result->each(function ($post) use ($mainPost, $category) {
$this->assertNotEquals($mainPost->id, $post->id);
$this->assertEquals($category->id, $post->category_id);
$this->assertEquals('published', $post->status);
});
}
/** @test */
public function it_searches_posts_by_query(): void
{
Post::factory()->published()->create([
'title' => 'Laravel Testing Best Practices',
'content' => 'Guide about PHP unit testing'
]);
Post::factory()->published()->create([
'title' => 'Vue.js Components',
'content' => 'Frontend JavaScript development'
]);
Post::factory()->published()->create([
'title' => 'Laravel API Development',
'content' => 'Building REST APIs with Laravel'
]);
$laravelResults = $this->postService->searchPosts('Laravel');
$testingResults = $this->postService->searchPosts('testing');
$apiResults = $this->postService->searchPosts('API');
$this->assertCount(2, $laravelResults);
$this->assertCount(1, $testingResults);
$this->assertCount(1, $apiResults);
}
}
4️⃣ Advanced Testing Patterns
Mock & Stub Testing:
<?php
// tests/Feature/EmailNotificationTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\{Post, User};
use App\Notifications\PostPublished;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\{Notification, Queue, Event};
use Illuminate\Notifications\AnonymousNotifiable;
class EmailNotificationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function post_publication_sends_notification_to_subscribers(): void
{
Notification::fake();
$author = User::factory()->create();
$subscribers = User::factory()->count(3)->create();
// Set up subscriptions
foreach ($subscribers as $subscriber) {
$author->subscribers()->attach($subscriber);
}
$post = Post::factory()->create(['user_id' => $author->id]);
// Publish the post
$post->update(['status' => 'published', 'published_at' => now()]);
// Assert notification was sent
Notification::assertSentTo(
$subscribers,
PostPublished::class,
function ($notification, $channels) use ($post) {
return $notification->post->id === $post->id;
}
);
}
/** @test */
public function queued_job_processes_email_sending(): void
{
Queue::fake();
$user = User::factory()->create();
$post = Post::factory()->create();
// Trigger job
dispatch(new \App\Jobs\SendPostNotification($post, $user));
Queue::assertPushed(\App\Jobs\SendPostNotification::class, function ($job) use ($post, $user) {
return $job->post->id === $post->id &&
$job->user->id === $user->id;
});
}
}
// tests/Unit/Services/PaymentServiceTest.php - Testing External APIs
namespace Tests\Unit\Services;
use Tests\TestCase;
use App\Services\PaymentService;
use Illuminate\Support\Facades\Http;
class PaymentServiceTest extends TestCase
{
/** @test */
public function it_processes_payment_successfully(): void
{
Http::fake([
'https://api.payment-provider.com/charges' => Http::response([
'id' => 'charge_123',
'status' => 'succeeded',
'amount' => 2000
], 200)
]);
$paymentService = new PaymentService();
$result = $paymentService->processPayment([
'amount' => 20.00,
'currency' => 'USD',
'token' => 'tok_visa'
]);
$this->assertTrue($result['success']);
$this->assertEquals('charge_123', $result['charge_id']);
Http::assertSent(function ($request) {
return $request->url() === 'https://api.payment-provider.com/charges' &&
$request['amount'] === 2000 &&
$request['currency'] === 'USD';
});
}
/** @test */
public function it_handles_payment_failure_gracefully(): void
{
Http::fake([
'https://api.payment-provider.com/charges' => Http::response([
'error' => ['message' => 'Card was declined']
], 402)
]);
$paymentService = new PaymentService();
$result = $paymentService->processPayment([
'amount' => 20.00,
'currency' => 'USD',
'token' => 'tok_declined'
]);
$this->assertFalse($result['success']);
$this->assertStringContains('declined', $result['error']);
}
}
Database Transaction Testing:
<?php
// tests/Feature/DatabaseTransactionTest.php
namespace Tests\Feature;
use Tests\Feature\DatabaseTestCase;
use App\Models\{User, Post, Comment};
use Illuminate\Support\Facades\DB;
class DatabaseTransactionTest extends DatabaseTestCase
{
/** @test */
public function it_rolls_back_transaction_on_failure(): void
{
$user = User::factory()->create();
$post = Post::factory()->create();
$initialCommentCount = Comment::count();
try {
DB::transaction(function () use ($user, $post) {
// Create a comment
Comment::create([
'user_id' => $user->id,
'post_id' => $post->id,
'content' => 'Test comment'
]);
// Simulate an error that causes rollback
throw new \Exception('Simulated error');
});
} catch (\Exception $e) {
// Expected exception
}
// Assert no comment was created due to rollback
$this->assertEquals($initialCommentCount, Comment::count());
$this->assertDatabaseMissing('comments', [
'content' => 'Test comment'
]);
}
/** @test */
public function it_commits_transaction_on_success(): void
{
$user = User::factory()->create();
$post = Post::factory()->create();
DB::transaction(function () use ($user, $post) {
Comment::create([
'user_id' => $user->id,
'post_id' => $post->id,
'content' => 'Successful comment'
]);
$post->increment('comments_count');
});
$this->assertDatabaseHas('comments', [
'content' => 'Successful comment'
]);
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'comments_count' => 1
]);
}
}
Next Steps
Optimize your Laravel applications for peak performance with Laravel Performance Optimization techniques and monitoring.
📩 Need help with Laravel testing?
Article Category
Testing in Laravel: Comprehensive Testing Strategies & Best Practices
Master Laravel testing with PHPUnit, Feature tests, Unit tests, Database testing, API testing, and advanced testing patterns for robust applications.
Consultation & Communication
Direct communication via WhatsApp or phone to understand your project needs precisely.
Planning & Scheduling
Creating clear work plan with specific timeline for each project phase.
Development & Coding
Building projects with latest technologies ensuring high performance and security.
Testing & Delivery
Comprehensive testing and thorough review before final project delivery.