دليل Laravel Testing الشامل

مرجع شامل لفهم وتطبيق الاختبارات في Laravel باستخدام PHPUnit واختبارات الميزات واختبارات الوحدة وأفضل ممارسات الاختبار

 

جدول المحتويات

1. مقدمة عن اختبارات Laravel

يوفر Laravel إمكانيات اختبار ممتازة جاهزة للاستخدام باستخدام PHPUnit. الاختبارات تضمن أن تطبيقك يعمل كما هو متوقع وتساعد في منع الأخطاء عند إجراء التغييرات.

اختبارات Laravel تتيح لك:

  • اختبار المكونات الفردية بمعزل عن بعضها (اختبارات الوحدة)
  • اختبار سير العمل الكامل للتطبيق (اختبارات الميزات)
  • اختبار طلبات واستجابات HTTP
  • اختبار عمليات قاعدة البيانات
  • محاكاة الخدمات الخارجية

2. الإعداد والتكوين

تكوين بيئة الاختبار

إنشاء ملف .env.testing في جذر المشروع:

# .env.testing
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_CONNECTION=sync

تشغيل الاختبارات

# Run all tests
php artisan test

# Run specific test file
php artisan test tests/Feature/PostTest.php

# Run tests with coverage
php artisan test --coverage

# Run specific test method
php artisan test --filter test_user_can_create_post

3. اختبارات الوحدة

اختبارات الوحدة تركز على اختبار الفئات والطرق الفردية بمعزل عن بعضها.

إنشاء اختبارات الوحدة

php artisan make:test UserServiceTest --unit

مثال أساسي على اختبار الوحدة

<!--?php namespace Tests\Unit; use App\Models\User; use App\Services\UserService; use Tests\TestCase; class UserServiceTest extends TestCase { public function test_can_format_user_full_name() { $user = new User([ 'first_name' => 'John',<br ?--> 'last_name' => 'Doe'
]);

$service = new UserService();
$fullName = $service->formatFullName($user);

$this->assertEquals('John Doe', $fullName);
}

public function test_validates_email_format()
{
$service = new UserService();

$this->assertTrue($service->isValidEmail('[email protected]'));
$this->assertFalse($service->isValidEmail('invalid-email'));
}
}

اختبار طرق النموذج

<!--?php namespace Tests\Unit; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostModelTest extends TestCase { use RefreshDatabase; public function test_post_slug_is_generated_from_title() { $post = new Post(['title' => 'This is a Test Title']);</p> <p> $this->assertEquals('this-is-a-test-title', $post->generateSlug());<br ?--> }

public function test_post_excerpt_truncates_content()
{
$content = str_repeat('This is a long content. ', 20);
$post = new Post(['content' => $content]);

$excerpt = $post->getExcerpt(50);

$this->assertLessThanOrEqual(50, strlen($excerpt));
$this->assertStringEndsWith('...', $excerpt);
}
}

4. اختبارات الميزات

اختبارات الميزات تختبر سير العمل الكامل للتطبيق من طلبات HTTP إلى الاستجابات.

إنشاء اختبارات الميزات

php artisan make:test PostControllerTest

مثال أساسي على اختبار الميزات

<!--?php namespace Tests\Feature; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostControllerTest extends TestCase { use RefreshDatabase; public function test_can_view_posts_index() { $posts = Post::factory()->count(3)->create(['is_published' => true]);</p> <p> $response = $this->get('/posts');</p> <p> $response->assertStatus(200);<br ?--> $response->assertViewIs('posts.index');
$response->assertSeeText($posts->first()->title);
}

public function test_can_view_single_post()
{
$post = Post::factory()->create([
'title' => 'Test Post Title',
'is_published' => true
]);

$response = $this->get("/posts/{$post->slug}");

$response->assertStatus(200);
$response->assertSeeText('Test Post Title');
$response->assertSeeText($post->content);
}

public function test_authenticated_user_can_create_post()
{
$user = User::factory()->create();

$postData = [
'title' => 'New Test Post',
'content' => 'This is the content of the test post.',
'is_published' => true
];

$response = $this->actingAs($user)->post('/posts', $postData);

$response->assertRedirect();
$this->assertDatabaseHas('posts', [
'title' => 'New Test Post',
'user_id' => $user->id
]);
}
}

اختبار التحقق من النماذج

<!--?php namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostValidationTest extends TestCase { use RefreshDatabase; public function test_title_is_required() { $user = User::factory()->create();</p> <p> $response = $this->actingAs($user)->post('/posts', [<br ?--> 'content' => 'Post content without title'
]);

$response->assertSessionHasErrors(['title']);
}

public function test_content_is_required()
{
$user = User::factory()->create();

$response = $this->actingAs($user)->post('/posts', [
'title' => 'Post Title'
]);

$response->assertSessionHasErrors(['content']);
}

public function test_valid_post_data_passes_validation()
{
$user = User::factory()->create();

$response = $this->actingAs($user)->post('/posts', [
'title' => 'Valid Post Title',
'content' => 'Valid post content',
'is_published' => true
]);

$response->assertSessionHasNoErrors();
}
}

5. اختبار قاعدة البيانات

ترحيلات قاعدة البيانات والمصانع

<!--?php namespace Tests\Feature; use App\Models\User; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class DatabaseTest extends TestCase { use RefreshDatabase; public function test_factory_creates_user() { $user = User::factory()->create([<br ?--> 'email' => '[email protected]'
]);

$this->assertDatabaseHas('users', [
'email' => '[email protected]'
]);

$this->assertInstanceOf(User::class, $user);
}

public function test_can_create_multiple_records()
{
User::factory()->count(5)->create();

$this->assertDatabaseCount('users', 5);
}

public function test_relationships_work()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);

$this->assertEquals($user->id, $post->user->id);
$this->assertTrue($user->posts->contains($post));
}
}

اختبار عمليات قاعدة البيانات

<!--?php namespace Tests\Feature; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class DatabaseOperationsTest extends TestCase { use RefreshDatabase; public function test_can_create_post() { $user = User::factory()->create();</p> <p> $post = Post::create([<br ?--> 'title' => 'Test Post',
'content' => 'Test content',
'user_id' => $user->id
]);

$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'user_id' => $user->id
]);
}

public function test_can_update_post()
{
$post = Post::factory()->create(['title' => 'Original Title']);

$post->update(['title' => 'Updated Title']);

$this->assertDatabaseHas('posts', [
'id' => $post->id,
'title' => 'Updated Title'
]);
}

public function test_can_delete_post()
{
$post = Post::factory()->create();
$postId = $post->id;

$post->delete();

$this->assertDatabaseMissing('posts', ['id' => $postId]);
}
}

6. اختبار HTTP

اختبار استجابات HTTP

<!--?php namespace Tests\Feature; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class HttpResponseTest extends TestCase { use RefreshDatabase; public function test_homepage_returns_successful_response() { $response = $this->get('/');</p> <p> $response->assertStatus(200);<br ?--> $response->assertOk();
}

public function test_nonexistent_route_returns_404()
{
$response = $this->get('/nonexistent-route');

$response->assertNotFound();
}

public function test_post_show_returns_correct_data()
{
$post = Post::factory()->create([
'title' => 'Test Post',
'content' => 'Test content'
]);

$response = $this->get("/posts/{$post->slug}");

$response->assertOk();
$response->assertSeeText('Test Post');
$response->assertSeeText('Test content');
}

public function test_redirect_works_correctly()
{
$response = $this->get('/old-url');

$response->assertRedirect('/new-url');
}
}

اختبار إرسال النماذج

<!--?php namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class FormSubmissionTest extends TestCase { use RefreshDatabase; public function test_contact_form_submission() { $formData = [ 'name' => 'John Doe',<br ?--> 'email' => '[email protected]',
'message' => 'Hello there!'
];

$response = $this->post('/contact', $formData);

$response->assertRedirect();
$response->assertSessionHas('success', 'Message sent successfully');
}

public function test_profile_update()
{
$user = User::factory()->create();

$updateData = [
'name' => 'Updated Name',
'email' => '[email protected]'
];

$response = $this->actingAs($user)->put('/profile', $updateData);

$response->assertRedirect();
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name',
'email' => '[email protected]'
]);
}
}

7. اختبار التوثيق

اختبار تسجيل الدخول والتسجيل

<!--?php namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Tests\TestCase; class AuthenticationTest extends TestCase { use RefreshDatabase; public function test_user_can_register() { $userData = [ 'name' => 'John Doe',<br ?--> 'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password'
];

$response = $this->post('/register', $userData);

$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'name' => 'John Doe',
'email' => '[email protected]'
]);
$this->assertAuthenticated();
}

public function test_user_can_login()
{
$user = User::factory()->create([
'email' => '[email protected]',
'password' => Hash::make('password')
]);

$response = $this->post('/login', [
'email' => '[email protected]',
'password' => 'password'
]);

$response->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
}

public function test_user_cannot_login_with_invalid_credentials()
{
$user = User::factory()->create();

$response = $this->post('/login', [
'email' => $user->email,
'password' => 'wrong-password'
]);

$response->assertSessionHasErrors();
$this->assertGuest();
}

public function test_user_can_logout()
{
$user = User::factory()->create();

$response = $this->actingAs($user)->post('/logout');

$response->assertRedirect('/');
$this->assertGuest();
}
}

اختبار التخويل

<!--?php namespace Tests\Feature; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class AuthorizationTest extends TestCase { use RefreshDatabase; public function test_guest_cannot_create_posts() { $response = $this->get('/posts/create');</p> <p> $response->assertRedirect('/login');<br ?--> }

public function test_user_can_only_edit_own_posts()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user1->id]);

// User can edit own post
$response = $this->actingAs($user1)->get("/posts/{$post->id}/edit");
$response->assertOk();

// User cannot edit other's post
$response = $this->actingAs($user2)->get("/posts/{$post->id}/edit");
$response->assertForbidden();
}

public function test_admin_can_access_admin_routes()
{
$admin = User::factory()->create(['role' => 'admin']);
$user = User::factory()->create(['role' => 'user']);

// Admin can access
$response = $this->actingAs($admin)->get('/admin/dashboard');
$response->assertOk();

// Regular user cannot access
$response = $this->actingAs($user)->get('/admin/dashboard');
$response->assertForbidden();
}
}

8. اختبار API

اختبار JSON APIs

<!--?php namespace Tests\Feature; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ApiTest extends TestCase { use RefreshDatabase; public function test_can_get_all_posts_via_api() { $posts = Post::factory()->count(3)->create();</p> <p> $response = $this->getJson('/api/posts');</p> <p> $response->assertOk()<br ?--> ->assertJsonCount(3, 'data')
->assertJsonStructure([
'data' => [
'*' => ['id', 'title', 'content', 'created_at']
]
]);
}

public function test_can_create_post_via_api()
{
$user = User::factory()->create();

$postData = [
'title' => 'API Created Post',
'content' => 'This post was created via API'
];

$response = $this->actingAs($user, 'sanctum')
->postJson('/api/posts', $postData);

$response->assertCreated()
->assertJson([
'title' => 'API Created Post'
]);

$this->assertDatabaseHas('posts', $postData);
}

public function test_api_validation_returns_422()
{
$user = User::factory()->create();

$response = $this->actingAs($user, 'sanctum')
->postJson('/api/posts', []);

$response->assertStatus(422);
$response->assertJsonValidationErrors(['title', 'content']);
}

public function test_api_requires_authentication()
{
$response = $this->postJson('/api/posts', [
'title' => 'Test Post',
'content' => 'Test content'
]);

$response->assertUnauthorized();
}
}

9. أساسيات المحاكاة

استخدام Laravel Fakes

<!--?php namespace Tests\Feature; use App\Models\User; use App\Notifications\WelcomeNotification; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Storage; use Tests\TestCase; class FakesTest extends TestCase { use RefreshDatabase; public function test_notification_is_sent() { Notification::fake(); $user = User::factory()->create();</p> <p> // Trigger notification<br ?--> $user->notify(new WelcomeNotification());

// Assert notification was sent
Notification::assertSentTo($user, WelcomeNotification::class);
}

public function test_email_is_sent()
{
Mail::fake();

$response = $this->post('/contact', [
'name' => 'John Doe',
'email' => '[email protected]',
'message' => 'Hello!'
]);

$response->assertOk();
Mail::assertSent(\App\Mail\ContactFormMail::class);
}

public function test_file_upload_works()
{
Storage::fake('public');
$user = User::factory()->create();

$response = $this->actingAs($user)->post('/upload', [
'file' => \Illuminate\Http\UploadedFile::fake()->image('avatar.jpg')
]);

$response->assertOk();
Storage::disk('public')->assertExists('avatars/avatar.jpg');
}
}

مثال بسيط على المحاكاة

<!--?php namespace Tests\Feature; use App\Models\User; use App\Services\PaymentService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; class PaymentTest extends TestCase { use RefreshDatabase; public function test_successful_payment() { $user = User::factory()->create();</p> <p> // Mock the payment service<br ?--> $paymentService = Mockery::mock(PaymentService::class);
$paymentService->shouldReceive('charge')
->once()
->with($user, 100.00)
->andReturn(['status' => 'success']);

$this->app->instance(PaymentService::class, $paymentService);

$response = $this->actingAs($user)->post('/payment/charge', [
'amount' => 100.00
]);

$response->assertOk()
->assertJson(['status' => 'success']);
}
}

10. أفضل الممارسات

تنظيم الاختبارات

  • استخدم أسماء طرق اختبار وصفية توضح ما يتم اختباره
  • اتبع نمط AAA: الترتيب، التنفيذ، التأكيد
  • اجعل الاختبارات بسيطة ومركزة على شيء واحد
  • استخدم المصانع بدلاً من إنشاء السجلات يدوياً

اختبار قاعدة البيانات

// Always use RefreshDatabase trait for database tests
class PostTest extends TestCase
{
use RefreshDatabase;

public function test_example()
{
// Test implementation
}
}

// Use factories for test data
$user = User::factory()->create(['email' => '[email protected]']);
$posts = Post::factory()->count(5)->create();

// Use specific assertions
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
$this->assertDatabaseCount('posts', 5);
$this->assertDatabaseMissing('posts', ['id' => 999]);

اختبار طلبات HTTP

// Test both success and failure scenarios
public function test_successful_login()
{
$user = User::factory()->create(['password' => Hash::make('password')]);

$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password'
]);

$response->assertRedirect('/dashboard');
$this->assertAuthenticated();
}

public function test_failed_login()
{
$response = $this->post('/login', [
'email' => '[email protected]',
'password' => 'wrong-password'
]);

$response->assertSessionHasErrors();
$this->assertGuest();
}

التأكيدات الشائعة

// HTTP Response Assertions
$response->assertOk(); // 200
$response->assertCreated(); // 201
$response->assertNoContent(); // 204
$response->assertNotFound(); // 404
$response->assertForbidden(); // 403
$response->assertUnauthorized(); // 401

// Database Assertions
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
$this->assertDatabaseMissing('users', ['email' => '[email protected]']);
$this->assertDatabaseCount('posts', 5);

// Authentication Assertions
$this->assertAuthenticated();
$this->assertAuthenticatedAs($user);
$this->assertGuest();

// View Assertions
$response->assertViewIs('posts.index');
$response->assertViewHas('posts');
$response->assertSeeText('Welcome');
$response->assertDontSeeText('Hidden');

// JSON Assertions
$response->assertJson(['status' => 'success']);
$response->assertJsonCount(3, 'data');
$response->assertJsonStructure(['data' => ['*' => ['id', 'name']]]);

بيئة الاختبار

// Use environment-specific configuration
// In .env.testing
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_CONNECTION=sync
MAIL_MAILER=array

// Create test-specific helpers
class TestCase extends BaseTestCase
{
protected function createUser($attributes = [])
{
return User::factory()->create($attributes);
}

protected function loginAs($user = null)
{
$user = $user ?: $this->createUser();
$this->actingAs($user);
return $user;
}
}

هذا الدليل يغطي الجوانب الأساسية لاختبارات Laravel، من اختبارات الوحدة الأساسية إلى اختبارات الميزات واختبار API. اتبع هذه الأنماط وأفض