اختبار التطبيقات في Laravel: دليل شامل للـ Testing
اختبار التطبيقات في Laravel: دليل شامل للـ Testing
دليل تخصصي من علاء عامر – مطور ومصمم مواقع وتطبيقات محترف
اختبار التطبيقات في Laravel أساسي لضمان جودة وموثوقية التطبيقات. تعلم كيفية كتابة اختبارات شاملة وفعالة.
2️⃣ Unit Tests المتقدمة
Base Test Class:
<?php
// tests/TestCase.php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, RefreshDatabase, WithFaker;
protected function setUp(): void
{
parent::setUp();
// إعداد إضافي للاختبارات
$this->artisan('db:seed', ['--class' => 'DatabaseSeeder']);
// إعداد الملفات المؤقتة للاختبار
\Storage::fake('public');
\Storage::fake('testing');
// إعداد Mail fake
\Mail::fake();
// إعداد Queue fake
\Queue::fake();
}
/**
* إنشاء مستخدم للاختبار
*/
protected function createUser(array $attributes = []): \App\Models\User
{
return \App\Models\User::factory()->create($attributes);
}
/**
* إنشاء admin للاختبار
*/
protected function createAdmin(array $attributes = []): \App\Models\User
{
$admin = $this->createUser($attributes);
$admin->assignRole('admin');
return $admin;
}
/**
* تسجيل دخول المستخدم
*/
protected function actingAsUser(\App\Models\User $user = null): static
{
$user = $user ?: $this->createUser();
return $this->actingAs($user);
}
/**
* تسجيل دخول admin
*/
protected function actingAsAdmin(\App\Models\User $admin = null): static
{
$admin = $admin ?: $this->createAdmin();
return $this->actingAs($admin);
}
/**
* إنشاء post للاختبار
*/
protected function createPost(array $attributes = []): \App\Models\Post
{
return \App\Models\Post::factory()->create($attributes);
}
/**
* assertion مخصص للJSON structure
*/
protected function assertJsonStructureExact(array $structure, $response)
{
$response->assertJsonStructure($structure);
$responseData = $response->json();
$this->assertEquals(
count($structure),
count($responseData),
'JSON response has extra or missing keys'
);
}
}
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;
class PostTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_belongs_to_a_user()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertInstanceOf(User::class, $post->author);
$this->assertEquals($user->id, $post->author->id);
}
/** @test */
public function it_belongs_to_a_category()
{
$category = Category::factory()->create();
$post = Post::factory()->create(['category_id' => $category->id]);
$this->assertInstanceOf(Category::class, $post->category);
$this->assertEquals($category->id, $post->category->id);
}
/** @test */
public function it_can_have_many_tags()
{
$post = Post::factory()->create();
$tags = Tag::factory()->count(3)->create();
$post->tags()->attach($tags);
$this->assertCount(3, $post->tags);
$this->assertContains($tags->first()->id, $post->tags->pluck('id'));
}
/** @test */
public function it_can_be_liked_by_users()
{
$post = Post::factory()->create();
$user = User::factory()->create();
$post->like($user);
$this->assertTrue($post->isLikedBy($user));
$this->assertEquals(1, $post->likes()->count());
}
/** @test */
public function it_can_be_unliked_by_users()
{
$post = Post::factory()->create();
$user = User::factory()->create();
$post->like($user);
$this->assertTrue($post->isLikedBy($user));
$post->unlike($user);
$this->assertFalse($post->isLikedBy($user));
$this->assertEquals(0, $post->likes()->count());
}
/** @test */
public function it_calculates_reading_time_correctly()
{
$content = str_repeat('word ', 200); // 200 كلمة
$post = Post::factory()->create(['content' => $content]);
// متوسط القراءة 200 كلمة في الدقيقة
$this->assertEquals(1, $post->reading_time);
}
/** @test */
public function it_generates_excerpt_from_content()
{
$longContent = str_repeat('This is a long content. ', 50);
$post = Post::factory()->create(['content' => $longContent]);
$excerpt = $post->generateExcerpt();
$this->assertTrue(strlen($excerpt) <= 200);
$this->assertStringEndsWith('...', $excerpt);
}
/** @test */
public function it_scopes_published_posts()
{
Post::factory()->create(['status' => 'published', 'published_at' => now()->subDay()]);
Post::factory()->create(['status' => 'draft']);
Post::factory()->create(['status' => 'published', 'published_at' => now()->addDay()]);
$publishedPosts = Post::published()->get();
$this->assertCount(1, $publishedPosts);
$this->assertEquals('published', $publishedPosts->first()->status);
}
/** @test */
public function it_scopes_recent_posts()
{
Post::factory()->create(['created_at' => now()->subWeeks(2)]);
Post::factory()->create(['created_at' => now()->subDays(3)]);
Post::factory()->create(['created_at' => now()->subDay()]);
$recentPosts = Post::recent(7)->get(); // آخر 7 أيام
$this->assertCount(2, $recentPosts);
}
/** @test */
public function it_increments_views_count()
{
$post = Post::factory()->create(['views_count' => 5]);
$user = User::factory()->create();
$post->incrementViews($user);
$this->assertEquals(6, $post->fresh()->views_count);
}
/** @test */
public function it_does_not_increment_views_for_same_user_repeatedly()
{
$post = Post::factory()->create(['views_count' => 5]);
$user = User::factory()->create();
$post->incrementViews($user);
$post->incrementViews($user); // نفس المستخدم مرة أخرى
$this->assertEquals(6, $post->fresh()->views_count);
}
/** @test */
public function it_formats_published_date_correctly()
{
$date = now()->subDays(5);
$post = Post::factory()->create(['published_at' => $date]);
$this->assertEquals(
$date->format('M j, Y'),
$post->formatted_date
);
}
/** @test */
public function it_checks_if_post_is_featured()
{
$featuredPost = Post::factory()->create(['is_featured' => true]);
$regularPost = Post::factory()->create(['is_featured' => false]);
$this->assertTrue($featuredPost->isFeatured());
$this->assertFalse($regularPost->isFeatured());
}
}
Service Class 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\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class PostServiceTest extends TestCase
{
private PostService $postService;
protected function setUp(): void
{
parent::setUp();
$this->postService = new PostService();
}
/** @test */
public function it_creates_post_with_valid_data()
{
$user = User::factory()->create();
$category = Category::factory()->create();
$postData = [
'title' => 'Test Post Title',
'content' => 'This is test content for the post.',
'category_id' => $category->id,
'status' => 'published',
'user_id' => $user->id
];
$post = $this->postService->createPost($postData);
$this->assertInstanceOf(Post::class, $post);
$this->assertEquals($postData['title'], $post->title);
$this->assertEquals('test-post-title', $post->slug);
$this->assertDatabaseHas('posts', [
'title' => $postData['title'],
'user_id' => $user->id
]);
}
/** @test */
public function it_handles_featured_image_upload()
{
Storage::fake('public');
$user = User::factory()->create();
$category = Category::factory()->create();
$image = UploadedFile::fake()->image('featured.jpg', 800, 600);
$postData = [
'title' => 'Post with Image',
'content' => 'Content here',
'category_id' => $category->id,
'user_id' => $user->id,
'featured_image' => $image
];
$post = $this->postService->createPost($postData);
$this->assertNotNull($post->featured_image);
Storage::disk('public')->assertExists($post->featured_image);
}
/** @test */
public function it_updates_post_correctly()
{
$post = Post::factory()->create(['title' => 'Original Title']);
$updateData = [
'title' => 'Updated Title',
'content' => 'Updated content'
];
$updatedPost = $this->postService->updatePost($post, $updateData);
$this->assertEquals('Updated Title', $updatedPost->title);
$this->assertEquals('updated-title', $updatedPost->slug);
$this->assertEquals('Updated content', $updatedPost->content);
}
/** @test */
public function it_generates_unique_slug()
{
Post::factory()->create(['slug' => 'duplicate-title']);
$slug = $this->postService->generateUniqueSlug('Duplicate Title');
$this->assertEquals('duplicate-title-1', $slug);
}
/** @test */
public function it_processes_tags_correctly()
{
$post = Post::factory()->create();
$tagNames = ['Laravel', 'PHP', 'Testing'];
$this->postService->syncTags($post, $tagNames);
$this->assertCount(3, $post->tags);
$this->assertTrue($post->tags->contains('name', 'Laravel'));
$this->assertTrue($post->tags->contains('name', 'PHP'));
$this->assertTrue($post->tags->contains('name', 'Testing'));
}
/** @test */
public function it_deletes_post_with_dependencies()
{
Storage::fake('public');
$image = UploadedFile::fake()->image('test.jpg');
$post = Post::factory()->create([
'featured_image' => $image->store('posts', 'public')
]);
$user = User::factory()->create();
$post->like($user);
$this->postService->deletePost($post);
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
$this->assertEquals(0, $post->likes()->count());
Storage::disk('public')->assertMissing($post->featured_image);
}
/** @test */
public function it_searches_posts_correctly()
{
Post::factory()->create(['title' => 'Laravel Tutorial', 'content' => 'Learn Laravel']);
Post::factory()->create(['title' => 'PHP Basics', 'content' => 'Learn PHP fundamentals']);
Post::factory()->create(['title' => 'JavaScript Guide', 'content' => 'JavaScript tutorial']);
$results = $this->postService->searchPosts('Laravel');
$this->assertCount(1, $results);
$this->assertEquals('Laravel Tutorial', $results->first()->title);
}
/** @test */
public function it_gets_related_posts()
{
$category = Category::factory()->create();
$mainPost = Post::factory()->create(['category_id' => $category->id]);
$relatedPost1 = Post::factory()->create(['category_id' => $category->id]);
$relatedPost2 = Post::factory()->create(['category_id' => $category->id]);
$unrelatedPost = Post::factory()->create(); // مختلف في التصنيف
$relatedPosts = $this->postService->getRelatedPosts($mainPost, 5);
$this->assertCount(2, $relatedPosts);
$this->assertTrue($relatedPosts->contains('id', $relatedPost1->id));
$this->assertTrue($relatedPosts->contains('id', $relatedPost2->id));
$this->assertFalse($relatedPosts->contains('id', $unrelatedPost->id));
}
}
4️⃣ Database Testing المتقدم
Factories & Seeders للاختبار:
<?php
// database/factories/PostFactory.php
namespace Database\Factories;
use App\Models\{Post, User, Category};
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition()
{
return [
'title' => $this->faker->sentence(),
'slug' => $this->faker->unique()->slug(),
'excerpt' => $this->faker->paragraph(),
'content' => $this->faker->paragraphs(5, true),
'featured_image' => 'posts/' . $this->faker->image('storage/app/public/posts', 800, 600, null, false),
'status' => $this->faker->randomElement(['draft', 'published']),
'is_featured' => $this->faker->boolean(20), // 20% فرصة أن يكون مميز
'published_at' => $this->faker->optional(0.8)->dateTimeBetween('-1 year', 'now'),
'views_count' => $this->faker->numberBetween(0, 1000),
'user_id' => User::factory(),
'category_id' => Category::factory(),
'meta_title' => $this->faker->optional()->sentence(),
'meta_description' => $this->faker->optional()->paragraph(),
'meta_keywords' => $this->faker->optional()->words(5, true),
];
}
/**
* مقال منشور
*/
public function published()
{
return $this->state(function (array $attributes) {
return [
'status' => 'published',
'published_at' => $this->faker->dateTimeBetween('-6 months', 'now'),
];
});
}
/**
* مقال مسودة
*/
public function draft()
{
return $this->state(function (array $attributes) {
return [
'status' => 'draft',
'published_at' => null,
];
});
}
/**
* مقال مميز
*/
public function featured()
{
return $this->state(function (array $attributes) {
return [
'is_featured' => true,
'status' => 'published',
'published_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
];
});
}
/**
* مقال بتصنيف معين
*/
public function withCategory(Category $category)
{
return $this->state(function (array $attributes) use ($category) {
return [
'category_id' => $category->id,
];
});
}
/**
* مقال بكاتب معين
*/
public function byAuthor(User $user)
{
return $this->state(function (array $attributes) use ($user) {
return [
'user_id' => $user->id,
];
});
}
}
// database/seeders/TestSeeder.php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\{User, Category, Post, Tag};
class TestSeeder extends Seeder
{
public function run()
{
// إنشاء المستخدمين
$admin = User::factory()->create([
'name' => 'Test Admin',
'email' => '[email protected]',
'email_verified_at' => now(),
]);
$admin->assignRole('admin');
$author = User::factory()->create([
'name' => 'Test Author',
'email' => '[email protected]',
'email_verified_at' => now(),
]);
$user = User::factory()->create([
'name' => 'Test User',
'email' => '[email protected]',
'email_verified_at' => now(),
]);
// إنشاء التصنيفات
$categories = Category::factory()->count(5)->create();
// إنشاء التصنيفات
$tags = Tag::factory()->count(10)->create();
// إنشاء المقالات
$posts = Post::factory()->count(20)->create();
// ربط المقالات بالتصنيفات
$posts->each(function ($post) use ($tags) {
$post->tags()->attach(
$tags->random(rand(1, 5))->pluck('id')->toArray()
);
});
// إنشاء تفاعلات
$posts->each(function ($post) use ($admin, $author, $user) {
// إعجابات عشوائية
collect([$admin, $author, $user])->random(rand(0, 3))->each(function ($u) use ($post) {
$post->like($u);
});
// مشاهدات عشوائية
for ($i = 0; $i < rand(10, 100); $i++) {
$post->incrementViews(collect([$admin, $author, $user])->random());
}
});
}
}
Database Transaction Tests:
<?php
// tests/Feature/DatabaseTransactionTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\{User, Post, Category};
use Illuminate\Support\Facades\DB;
class DatabaseTransactionTest extends TestCase
{
/** @test */
public function post_creation_is_transactional()
{
$user = User::factory()->create();
$category = Category::factory()->create();
// محاكاة فشل في إنشاء مقال
DB::shouldReceive('beginTransaction')->once();
DB::shouldReceive('rollBack')->once();
DB::shouldReceive('commit')->never();
try {
DB::transaction(function () use ($user, $category) {
$post = Post::create([
'title' => 'Test Post',
'content' => 'Test content',
'user_id' => $user->id,
'category_id' => $category->id,
]);
// محاكاة خطأ
throw new \Exception('Simulated error');
});
} catch (\Exception $e) {
// التحقق أن المقال لم يُحفظ بسبب rollback
$this->assertDatabaseMissing('posts', [
'title' => 'Test Post',
'user_id' => $user->id
]);
}
}
/** @test */
public function bulk_operations_maintain_data_integrity()
{
$users = User::factory()->count(10)->create();
$category = Category::factory()->create();
DB::transaction(function () use ($users, $category) {
$users->each(function ($user) use ($category) {
Post::factory()->count(5)->create([
'user_id' => $user->id,
'category_id' => $category->id,
]);
});
});
$this->assertEquals(50, Post::count());
$this->assertEquals(10, User::count());
// التحقق من سلامة العلاقات
$users->each(function ($user) {
$this->assertEquals(5, $user->posts()->count());
});
}
}
💡 أفضل الممارسات للاختبار
- اكتب اختبارات سريعة وموثوقة
- استخدم Factories لإنشاء البيانات الوهمية
- اختبر الحالات الاستثنائية والأخطاء
- حافظ على نظافة البيانات بين الاختبارات
- استخدم Test Doubles للخدمات الخارجية
- اكتب اختبارات للأمان والصلاحيات
- راقب تغطية الكود وحسّنها
الخطوة التالية
تعلم Performance Optimization في Laravel لتحسين أداء تطبيقاتك.
📩 هل تحتاج مساعدة في إعداد بيئة الاختبار؟
قسم المقالة
اختبار التطبيقات في Laravel: دليل شامل للـ Testing
دليل متقدم لاختبار تطبيقات Laravel باستخدام PHPUnit وFeature Tests، من الاختبارات الأساسية إلى استراتيجيات TDD المتقدمة.
التواصل والاستشارة
تواصل مباشر عبر الواتساب أو الهاتف لفهم احتياجات مشروعك بدقة.
التخطيط والجدولة
وضع خطة عمل واضحة مع جدول زمني محدد لكل مرحلة من المشروع.
البرمجة والتطوير
تطوير المشروع بأحدث التقنيات لضمان الأداء والأمان العاليين.
المراجعة والتسليم
ختبار شامل ومراجعة دقيقة قبل التسليم النهائي للمشروع.