تطوير واجهات REST API

اختبار API المتقدم

20 دقيقة الدرس 21 من 35

اختبار API المتقدم

الاختبار أمر بالغ الأهمية لبناء واجهات برمجية موثوقة. في هذا الدرس، سنستكشف استراتيجيات الاختبار المتقدمة بما في ذلك اختبار العقود، اختبار التكامل، استخدام Pest PHP لاختبار API، ومحاكاة الخدمات الخارجية.

لماذا يهم الاختبار المتقدم

اختبارات الوحدة الأساسية ليست كافية لواجهات API. تحتاج للتحقق من:

  • الامتثال للعقد: هل يلبي API المواصفات الموثقة؟
  • سلوك التكامل: هل تعمل جميع المكونات معاً بشكل صحيح؟
  • التبعيات الخارجية: كيف يتصرف API عندما تفشل خدمات الطرف الثالث؟
  • الأداء: هل يمكن لـ API التعامل مع الحمل المتوقع؟
  • الأمان: هل تعمل المصادقة والترخيص بشكل صحيح؟
هرم الاختبار: اتبع نهج هرم الاختبار: العديد من اختبارات الوحدة، بعض اختبارات التكامل، عدد قليل من الاختبارات الشاملة. هذا يضمن ردود فعل سريعة مع الحفاظ على تغطية شاملة.

اختبار العقود باستخدام OpenAPI

اختبار العقود يضمن أن تنفيذ API الخاص بك يطابق مواصفات OpenAPI. هذا يمنع التغييرات الجذرية ويحافظ على اتساق API.

<?php // tests/Feature/ContractTest.php namespace Tests\Feature; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class ContractTest extends TestCase { use RefreshDatabase; /** @test */ public function users_endpoint_matches_openapi_contract() { // إنشاء بيانات اختبار User::factory()->count(3)->create(); // تنفيذ الطلب $response = $this->getJson('/api/v1/users'); // التحقق مقابل مخطط OpenAPI $response->assertStatus(200) ->assertJsonStructure([ 'data' => [ '*' => [ 'id', 'type', 'attributes' => [ 'name', 'email', 'created_at', 'updated_at' ], 'links' => [ 'self' ] ] ], 'links' => [ 'self', 'first', 'last' ], 'meta' => [ 'total', 'per_page', 'current_page' ] ]); // التحقق من أنواع الاستجابة $data = $response->json('data.0'); $this->assertIsInt($data['id']); $this->assertEquals('users', $data['type']); $this->assertIsString($data['attributes']['email']); } /** @test */ public function validation_errors_match_openapi_contract() { $response = $this->postJson('/api/v1/users', [ 'email' => 'invalid-email' ]); $response->assertStatus(422) ->assertJsonStructure([ 'errors' => [ '*' => [ 'status', 'title', 'detail', 'source' => [ 'pointer' ] ] ] ]); } }
التحقق التلقائي من العقد: استخدم أدوات مثل Spectator (حزمة Laravel) للتحقق تلقائياً من الاستجابات مقابل مواصفات OpenAPI الخاصة بك دون كتابة تأكيدات يدوية.

استراتيجيات اختبار التكامل

اختبارات التكامل تتحقق من أن المكونات المتعددة تعمل معاً بشكل صحيح. تختبر دورة الطلب/الاستجابة الكاملة بما في ذلك عمليات قاعدة البيانات والوسيطات والخدمات الخارجية.

<?php // tests/Feature/OrderIntegrationTest.php namespace Tests\Feature; use Tests\TestCase; use App\Models\User; use App\Models\Product; use App\Events\OrderPlaced; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; use Illuminate\Foundation\Testing\RefreshDatabase; class OrderIntegrationTest extends TestCase { use RefreshDatabase; /** @test */ public function complete_order_workflow_executes_successfully() { // محاكاة الأحداث والطوابير Event::fake([OrderPlaced::class]); Queue::fake(); // إنشاء بيانات اختبار $user = User::factory()->create(); $product = Product::factory()->create([ 'price' => 99.99, 'stock' => 10 ]); // المصادقة $token = $user->createToken('test')->plainTextToken; // تقديم الطلب $response = $this->withToken($token) ->postJson('/api/v1/orders', [ 'items' => [ [ 'product_id' => $product->id, 'quantity' => 2 ] ], 'shipping_address' => [ 'street' => '123 Main St', 'city' => 'New York', 'postal_code' => '10001', 'country' => 'US' ] ]); // التأكد من الاستجابة $response->assertStatus(201) ->assertJsonStructure([ 'data' => [ 'id', 'type', 'attributes' => [ 'status', 'total_amount', 'items_count', 'created_at' ] ] ]); // التأكد من تغييرات قاعدة البيانات $this->assertDatabaseHas('orders', [ 'user_id' => $user->id, 'status' => 'pending', 'total_amount' => 199.98 ]); $this->assertDatabaseHas('order_items', [ 'product_id' => $product->id, 'quantity' => 2, 'unit_price' => 99.99 ]); // التأكد من تقليل المخزون $this->assertEquals(8, $product->fresh()->stock); // التأكد من إرسال الحدث Event::assertDispatched(OrderPlaced::class, function ($event) use ($response) { return $event->order->id === $response->json('data.id'); }); // التأكد من إضافة الإشعار إلى الطابور Queue::assertPushed(SendOrderConfirmationNotification::class); } /** @test */ public function order_fails_with_insufficient_stock() { $user = User::factory()->create(); $product = Product::factory()->create([ 'price' => 99.99, 'stock' => 1 ]); $token = $user->createToken('test')->plainTextToken; $response = $this->withToken($token) ->postJson('/api/v1/orders', [ 'items' => [ [ 'product_id' => $product->id, 'quantity' => 2 ] ] ]); $response->assertStatus(422) ->assertJsonPath('errors.0.detail', 'Insufficient stock for product'); // التأكد من عدم إنشاء طلب $this->assertDatabaseCount('orders', 0); // التأكد من عدم تغيير المخزون $this->assertEquals(1, $product->fresh()->stock); } }

Pest PHP لاختبار API

Pest PHP هو إطار اختبار حديث يجعل الاختبارات أكثر قابلية للقراءة وممتعة للكتابة. إنه مناسب بشكل خاص لاختبار API بفضل صياغته المعبرة.

<?php // tests/Feature/UserApiTest.php use App\Models\User; it('lists all users with pagination', function () { User::factory()->count(25)->create(); $response = $this->getJson('/api/v1/users?page=2&per_page=10'); expect($response->status())->toBe(200) ->and($response->json('data'))->toHaveCount(10) ->and($response->json('meta.current_page'))->toBe(2) ->and($response->json('meta.total'))->toBe(25); }); it('creates a user successfully', function () { $userData = [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'Password123!', 'password_confirmation' => 'Password123!' ]; $response = $this->postJson('/api/v1/users', $userData); expect($response->status())->toBe(201) ->and($response->json('data.attributes.email'))->toBe('john@example.com'); }); it('validates required fields', function () { $response = $this->postJson('/api/v1/users', []); expect($response->status())->toBe(422) ->and($response->json('errors'))->toBeArray() ->and($response->json('errors.0.source.pointer'))->toContain('name'); }); test('authenticated user can update their profile', function () { $user = User::factory()->create(); $token = $user->createToken('test')->plainTextToken; $response = $this->withToken($token) ->patchJson("/api/v1/users/{$user->id}", [ 'name' => 'Updated Name' ]); expect($response->status())->toBe(200) ->and($user->fresh()->name)->toBe('Updated Name'); }); test('user cannot update another user\'s profile', function () { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $token = $user1->createToken('test')->plainTextToken; $response = $this->withToken($token) ->patchJson("/api/v1/users/{$user2->id}", [ 'name' => 'Hacked Name' ]); expect($response->status())->toBe(403); }); // استخدام مجموعات البيانات لحالات اختبار متعددة it('validates email format', function (string $email, bool $valid) { $response = $this->postJson('/api/v1/users', [ 'name' => 'John Doe', 'email' => $email, 'password' => 'Password123!', 'password_confirmation' => 'Password123!' ]); if ($valid) { expect($response->status())->toBe(201); } else { expect($response->status())->toBe(422); } })->with([ ['valid@example.com', true], ['invalid-email', false], ['missing@domain', false], ['@nodomain.com', false], ['spaces in@email.com', false] ]);
فوائد Pest: صيغة expect() في Pest أكثر قابلية للقراءة من تأكيدات PHPUnit، وميزات مثل مجموعات البيانات تقلل بشكل كبير من تكرار الكود.

محاكاة الخدمات الخارجية

عندما يعتمد API الخاص بك على خدمات خارجية (بوابات الدفع، موفري البريد الإلكتروني، واجهات API لطرف ثالث)، تحتاج إلى محاكاة هذه التبعيات لضمان اختبارات سريعة وموثوقة لا تعتمد على توافر خارجي.

<?php // tests/Feature/PaymentProcessingTest.php namespace Tests\Feature; use Tests\TestCase; use App\Models\Order; use App\Services\StripeService; use Illuminate\Support\Facades\Http; use Illuminate\Foundation\Testing\RefreshDatabase; class PaymentProcessingTest extends TestCase { use RefreshDatabase; /** @test */ public function payment_processes_successfully_with_stripe() { // محاكاة استجابات Stripe API Http::fake([ 'api.stripe.com/v1/payment_intents' => Http::response([ 'id' => 'pi_test123', 'status' => 'succeeded', 'amount' => 10000, 'currency' => 'usd' ], 200) ]); $order = Order::factory()->create([ 'total_amount' => 100.00, 'status' => 'pending' ]); $response = $this->postJson("/api/v1/orders/{$order->id}/payments", [ 'payment_method' => 'card', 'payment_method_id' => 'pm_test_card' ]); $response->assertStatus(200) ->assertJson([ 'data' => [ 'attributes' => [ 'status' => 'paid', 'payment_intent_id' => 'pi_test123' ] ] ]); // التحقق من تنفيذ طلب HTTP Http::assertSent(function ($request) { return $request->url() === 'https://api.stripe.com/v1/payment_intents' && $request['amount'] === 10000; }); } /** @test */ public function payment_fails_gracefully_when_stripe_is_down() { // محاكاة فشل الشبكة Http::fake([ 'api.stripe.com/*' => Http::response(null, 500) ]); $order = Order::factory()->create([ 'total_amount' => 100.00, 'status' => 'pending' ]); $response = $this->postJson("/api/v1/orders/{$order->id}/payments", [ 'payment_method' => 'card', 'payment_method_id' => 'pm_test_card' ]); $response->assertStatus(503) ->assertJsonPath('errors.0.title', 'Payment service temporarily unavailable'); // التحقق من عدم تغيير حالة الطلب $this->assertEquals('pending', $order->fresh()->status); } } // المحاكاة باستخدام واجهة خدمة interface PaymentGateway { public function charge(float $amount, string $paymentMethod): array; } // tests/Feature/PaymentWithMockServiceTest.php class PaymentWithMockServiceTest extends TestCase { use RefreshDatabase; /** @test */ public function payment_uses_mocked_gateway() { // إنشاء محاكاة $mockGateway = $this->createMock(PaymentGateway::class); // تعيين التوقعات $mockGateway->expects($this->once()) ->method('charge') ->with(100.00, 'pm_test_card') ->willReturn([ 'success' => true, 'transaction_id' => 'txn_123' ]); // ربط المحاكاة بالحاوية $this->app->instance(PaymentGateway::class, $mockGateway); // تنفيذ الاختبار $order = Order::factory()->create(['total_amount' => 100.00]); $response = $this->postJson("/api/v1/orders/{$order->id}/payments", [ 'payment_method_id' => 'pm_test_card' ]); $response->assertStatus(200); } }

أفضل ممارسات الاختبار

تنظيم الاختبارات:
  • استخدم أسماء اختبار وصفية تشرح ما يتم اختباره
  • اتبع نمط الترتيب-التنفيذ-التأكيد
  • تأكيد واحد لكل اختبار (عندما يكون ذلك ممكناً)
  • حافظ على استقلالية الاختبارات وعزلها
  • استخدم المصانع لتوليد بيانات الاختبار
<?php // مثال على بنية اختبار جيدة /** @test */ public function authenticated_user_can_create_post() { // الترتيب: إعداد بيانات وظروف الاختبار $user = User::factory()->create(); $token = $user->createToken('test')->plainTextToken; $postData = [ 'title' => 'Test Post', 'content' => 'This is test content', 'status' => 'published' ]; // التنفيذ: تنفيذ الإجراء المُختبر $response = $this->withToken($token) ->postJson('/api/v1/posts', $postData); // التأكيد: التحقق من النتيجة المتوقعة $response->assertStatus(201) ->assertJsonPath('data.attributes.title', 'Test Post'); $this->assertDatabaseHas('posts', [ 'user_id' => $user->id, 'title' => 'Test Post' ]); }

اختبار الأداء

اختبر أداء API الخاص بك تحت الحمل لتحديد نقاط الاختناق:

<?php // tests/Performance/ApiPerformanceTest.php namespace Tests\Performance; use Tests\TestCase; use App\Models\User; class ApiPerformanceTest extends TestCase { /** @test */ public function api_responds_within_acceptable_time() { User::factory()->count(100)->create(); $startTime = microtime(true); $response = $this->getJson('/api/v1/users'); $duration = microtime(true) - $startTime; $response->assertStatus(200); // التأكد من أن زمن الاستجابة أقل من 500 مللي ثانية $this->assertLessThan(0.5, $duration, "استغرقت استجابة API {$duration} ثانية، متوقع أقل من 0.5 ثانية"); } /** @test */ public function database_queries_are_optimized() { User::factory()->count(50)->create(); $this->assertQueryCount(2, function () { $this->getJson('/api/v1/users'); }); } protected function assertQueryCount(int $expected, callable $callback) { $queries = 0; DB::listen(function ($query) use (&$queries) { $queries++; }); $callback(); $this->assertEquals($expected, $queries, "متوقع {$expected} استعلامات، لكن تم تنفيذ {$queries}"); } }
تمرين:
  1. اكتب اختبارات تكامل لتدفق تسجيل مستخدم كامل بما في ذلك التحقق من البريد الإلكتروني
  2. قم بتحويل اختبار PHPUnit موجود إلى صيغة Pest PHP
  3. حاكي API طقس خارجي واختبر معالجة الأخطاء عندما يفشل
  4. أنشئ اختبار أداء يضمن أن API الخاص بك يمكنه التعامل مع 100 طلب متزامن
  5. اكتب اختبارات عقود تتحقق من استجابات API الخاصة بك مقابل مخطط OpenAPI
أخطاء الاختبار الشائعة:
  • عدم عزل الاختبارات - الاختبارات التي تعتمد على بعضها البعض هشة
  • اختبار تفاصيل التنفيذ بدلاً من السلوك
  • عدم تنظيف بيانات الاختبار بشكل صحيح
  • إجراء طلبات HTTP حقيقية للخدمات الخارجية
  • تجاهل الحالات الحدية وسيناريوهات الأخطاء