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

أساسيات اختبار واجهات برمجة التطبيقات

15 دقيقة الدرس 20 من 50

مقدمة في اختبار واجهات برمجة التطبيقات

يُعد اختبار واجهة برمجة التطبيقات REST أمرًا بالغ الأهمية لضمان الموثوقية واكتشاف الأخطاء مبكرًا والحفاظ على جودة الكود مع نمو تطبيقك. في هذا الدرس الشامل، سنستكشف كيفية كتابة اختبارات فعالة لواجهات برمجة التطبيقات باستخدام PHPUnit في Laravel، بدءًا من اختبار نقاط النهاية الأساسية إلى التفاعلات المتقدمة مع قاعدة البيانات واختبار المصادقة.

يتحقق اختبار واجهة برمجة التطبيقات من أن نقاط النهاية تُرجع الاستجابات الصحيحة، وتتعامل مع الأخطاء بشكل صحيح، وتتحقق من المدخلات بشكل صحيح، وتحافظ على سلامة البيانات. تعمل الاختبارات الآلية كوثائق حية وتوفر الثقة عند إعادة هيكلة الكود أو إضافة ميزات جديدة.

إعداد بيئة الاختبار

يأتي Laravel مع PHPUnit مُعد مسبقًا. تستخدم بيئة الاختبار الخاصة بك تكوينًا منفصلاً محددًا في phpunit.xml في جذر مشروعك:

<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true"> <testsuites> <testsuite name="Feature"> <directory suffix="Test.php">./tests/Feature</directory> </testsuite> </testsuites> <php> <env name="APP_ENV" value="testing"/> <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> <env name="CACHE_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/> <env name="QUEUE_DRIVER" value="sync"/> </php> </phpunit>
تكوين بيئة الاختبار: تستخدم بيئة الاختبار عادةً قاعدة بيانات SQLite في الذاكرة للسرعة والعزل. يتم تشغيل كل اختبار في حالة قاعدة بيانات جديدة، مما يضمن عدم تأثير الاختبارات على بعضها البعض.

يوفر Laravel نوعين من الاختبارات:

  • اختبارات الوحدة (Unit Tests): اختبار الدوال أو الفئات الفردية بشكل منعزل (مخزنة في tests/Unit)
  • اختبارات الميزات (Feature Tests): اختبار الميزات الكاملة بما في ذلك طلبات HTTP والتفاعلات مع قاعدة البيانات ومكونات متعددة تعمل معًا (مخزنة في tests/Feature)

لاختبار واجهة برمجة التطبيقات، نستخدم بشكل أساسي اختبارات الميزات لأنها تختبر دورة الطلب-الاستجابة بأكملها.

إنشاء اختبار واجهة برمجة التطبيقات الأول

أنشئ ملف اختبار جديد باستخدام Artisan:

php artisan make:test Api/ProductTest

ينشئ هذا tests/Feature/Api/ProductTest.php. إليك الهيكل الأساسي:

<?php namespace Tests\Feature\Api; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ProductTest extends TestCase { use RefreshDatabase; public function test_can_retrieve_products_list() { // Arrange: إعداد بيانات الاختبار // Act: إجراء طلب واجهة برمجة التطبيقات // Assert: التحقق من الاستجابة } }
نمط AAA: قم بتنظيم اختباراتك باستخدام نمط Arrange-Act-Assert. أولاً قم بترتيب بيانات الاختبار الخاصة بك، ثم تصرف بإجراء الطلب، وأخيرًا تأكد من النتيجة المتوقعة. هذا يجعل الاختبارات قابلة للقراءة وسهلة الصيانة.

سمة RefreshDatabase

سمة RefreshDatabase ضرورية لاختبار قاعدة البيانات. تضمن أن كل اختبار يبدأ بقاعدة بيانات نظيفة عن طريق تشغيل الترحيلات قبل الاختبارات والتراجع عن التغييرات بعد كل اختبار:

<?php namespace Tests\Feature\Api; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Product; class ProductTest extends TestCase { use RefreshDatabase; public function test_database_is_fresh_for_each_test() { // يعمل هذا الاختبار بقاعدة بيانات فارغة $this->assertDatabaseCount('products', 0); // إنشاء منتج Product::factory()->create(); $this->assertDatabaseCount('products', 1); } public function test_database_is_still_fresh() { // على الرغم من أن الاختبار السابق أنشأ منتجًا، // يبدأ هذا الاختبار جديدًا بصفر منتجات $this->assertDatabaseCount('products', 0); } }

بدون RefreshDatabase، ستستمر البيانات من اختبار واحد وتؤثر على الاختبارات الأخرى، مما يتسبب في فشل غير متوقع. تضمن هذه السمة عزل الاختبار وقابلية التكرار.

تأكيدات HTTP الأساسية

يوفر Laravel طرقًا بديهية لاختبار استجابات HTTP. الأكثر أساسية هي assertStatus() و assertJson():

<?php public function test_can_retrieve_product() { // إنشاء منتج اختباري $product = Product::factory()->create([ 'name' => 'منتج تجريبي', 'price' => 99.99, 'stock' => 10 ]); // إجراء طلب GET لاسترجاع المنتج $response = $this->getJson("/api/products/{$product->id}"); // التأكد من أن حالة الاستجابة هي 200 OK $response->assertStatus(200); // التأكد من أن استجابة JSON تحتوي على البيانات المتوقعة $response->assertJson([ 'data' => [ 'id' => $product->id, 'name' => 'منتج تجريبي', 'price' => 99.99, 'stock' => 10 ] ]); }

تأكيدات أكواد حالة HTTP الشائعة:

// استجابات النجاح $response->assertOk(); // 200 $response->assertCreated(); // 201 $response->assertNoContent(); // 204 // استجابات خطأ العميل $response->assertBadRequest(); // 400 $response->assertUnauthorized(); // 401 $response->assertForbidden(); // 403 $response->assertNotFound(); // 404 $response->assertUnprocessable(); // 422 // استجابات خطأ الخادم $response->assertServerError(); // 500 // أكواد الحالة المخصصة $response->assertStatus(418); // أنا إبريق شاي

تأكيدات بنية JSON

تتحقق طريقة assertJsonStructure() من شكل استجابة JSON دون التحقق من القيم الدقيقة. هذا مفيد لضمان استجابات واجهة برمجة التطبيقات المتسقة:

<?php public function test_products_list_has_correct_structure() { // إنشاء منتجات متعددة Product::factory()->count(3)->create(); $response = $this->getJson('/api/products'); // التأكد من أن الاستجابة لها البنية المتوقعة $response->assertJsonStructure([ 'data' => [ '*' => [ // * تعني كل عنصر في المصفوفة 'id', 'name', 'description', 'price', 'stock', 'created_at', 'updated_at' ] ], 'links' => [ 'first', 'last', 'prev', 'next' ], 'meta' => [ 'current_page', 'last_page', 'per_page', 'total' ] ]); }

يمكنك أيضًا التحقق من البنى المتداخلة:

<?php public function test_product_with_category_structure() { $product = Product::factory() ->for(Category::factory()) ->create(); $response = $this->getJson("/api/products/{$product->id}"); $response->assertJsonStructure([ 'data' => [ 'id', 'name', 'price', 'category' => [ // بنية متداخلة 'id', 'name', 'slug' ] ] ]); }

تأكيدات JSON المتقدمة

يوفر Laravel العديد من تأكيدات JSON المتخصصة لسيناريوهات اختبار مختلفة:

<?php public function test_advanced_json_assertions() { $product = Product::factory()->create([ 'name' => 'حاسوب محمول متميز', 'price' => 1299.99 ]); $response = $this->getJson("/api/products/{$product->id}"); // التأكد من أن الاستجابة تحتوي على جزء JSON محدد $response->assertJsonFragment([ 'name' => 'حاسوب محمول متميز' ]); // التأكد من أن الاستجابة هي بالضبط هذا JSON $response->assertExactJson([ 'data' => [ 'id' => $product->id, 'name' => 'حاسوب محمول متميز', 'price' => 1299.99 ] ]); // التأكد من أن الاستجابة لا تحتوي على شيء ما $response->assertJsonMissing([ 'secret_key' => 'لا-يجب-أن-يكون-مكشوف' ]); // التأكد من أن مسار JSON له قيمة محددة $response->assertJsonPath('data.name', 'حاسوب محمول متميز'); // التأكد من عدد العناصر في مصفوفة $response->assertJsonCount(1, 'data'); }
كن حذرًا مع assertExactJson: يتطلب هذا التأكيد أن يطابق JSON تمامًا، بما في ذلك ترتيب المفاتيح وجميع الحقول. إنه صارم جدًا ويمكن أن يجعل الاختبارات هشة. استخدم assertJson() أو assertJsonFragment() لاختبار أكثر مرونة.

استخدام المصانع لبيانات الاختبار

تسمح لك المصانع بإنشاء بيانات وهمية بسرعة. حدد المصانع في database/factories:

<?php namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; class ProductFactory extends Factory { public function definition(): array { return [ 'name' => fake()->words(3, true), 'description' => fake()->paragraph(), 'price' => fake()->randomFloat(2, 10, 1000), 'stock' => fake()->numberBetween(0, 100), 'category_id' => Category::factory(), 'is_active' => true ]; } // حالة مصنع مخصصة للمنتجات غير المتوفرة في المخزون public function outOfStock(): static { return $this->state(fn (array $attributes) => [ 'stock' => 0, ]); } // حالة مصنع مخصصة للمنتجات غير النشطة public function inactive(): static { return $this->state(fn (array $attributes) => [ 'is_active' => false, ]); } }

استخدم المصانع في اختباراتك:

<?php public function test_using_factories() { // إنشاء منتج واحد بسمات افتراضية $product = Product::factory()->create(); // إنشاء منتجات متعددة $products = Product::factory()->count(5)->create(); // إنشاء بسمات مخصصة $expensiveProduct = Product::factory()->create([ 'price' => 9999.99 ]); // استخدام حالات المصنع المخصصة $outOfStock = Product::factory()->outOfStock()->create(); // إنشاء بدون حفظ في قاعدة البيانات (لاختبار المنطق) $unsavedProduct = Product::factory()->make(); // إنشاء مع العلاقات $productWithCategory = Product::factory() ->for(Category::factory()) ->create(); }
حالات المصنع: حدد حالات المصنع المخصصة للسيناريوهات الشائعة مثل "غير متوفر في المخزون"، "مخفض" أو "مميز". هذا يجعل اختباراتك أكثر تعبيرًا وأسهل في الفهم.

اختبار عمليات CRUD

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

<?php namespace Tests\Feature\Api; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Product; use App\Models\Category; class ProductCrudTest extends TestCase { use RefreshDatabase; // CREATE (POST) public function test_can_create_product() { $category = Category::factory()->create(); $productData = [ 'name' => 'منتج جديد', 'description' => 'وصف تجريبي', 'price' => 49.99, 'stock' => 20, 'category_id' => $category->id ]; $response = $this->postJson('/api/products', $productData); $response->assertCreated() ->assertJsonFragment([ 'name' => 'منتج جديد', 'price' => 49.99 ]); // التحقق من حفظ المنتج في قاعدة البيانات $this->assertDatabaseHas('products', [ 'name' => 'منتج جديد', 'price' => 49.99 ]); } // READ (GET) public function test_can_retrieve_product() { $product = Product::factory()->create(); $response = $this->getJson("/api/products/{$product->id}"); $response->assertOk() ->assertJson([ 'data' => [ 'id' => $product->id, 'name' => $product->name ] ]); } public function test_can_retrieve_products_list() { Product::factory()->count(3)->create(); $response = $this->getJson('/api/products'); $response->assertOk() ->assertJsonCount(3, 'data'); } // UPDATE (PUT/PATCH) public function test_can_update_product() { $product = Product::factory()->create([ 'name' => 'اسم قديم', 'price' => 99.99 ]); $updateData = [ 'name' => 'اسم محدث', 'price' => 149.99 ]; $response = $this->putJson("/api/products/{$product->id}", $updateData); $response->assertOk() ->assertJsonFragment([ 'name' => 'اسم محدث', 'price' => 149.99 ]); // التحقق من تحديث قاعدة البيانات $this->assertDatabaseHas('products', [ 'id' => $product->id, 'name' => 'اسم محدث', 'price' => 149.99 ]); // التحقق من أن البيانات القديمة لم تعد موجودة $this->assertDatabaseMissing('products', [ 'id' => $product->id, 'name' => 'اسم قديم' ]); } // DELETE (DELETE) public function test_can_delete_product() { $product = Product::factory()->create(); $response = $this->deleteJson("/api/products/{$product->id}"); $response->assertNoContent(); // التحقق من حذف المنتج $this->assertDatabaseMissing('products', [ 'id' => $product->id ]); // إذا كنت تستخدم الحذف الناعم $this->assertSoftDeleted('products', [ 'id' => $product->id ]); } public function test_returns_404_for_non_existent_product() { $response = $this->getJson('/api/products/99999'); $response->assertNotFound(); } }

اختبار قواعد التحقق

اختبار أن واجهة برمجة التطبيقات الخاصة بك تتحقق من المدخلات بشكل صحيح أمر بالغ الأهمية للأمان وسلامة البيانات:

<?php public function test_product_creation_requires_name() { $response = $this->postJson('/api/products', [ 'price' => 99.99, 'stock' => 10 // الاسم مفقود ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['name']); } public function test_product_price_must_be_numeric() { $response = $this->postJson('/api/products', [ 'name' => 'منتج تجريبي', 'price' => 'ليس-رقم', 'stock' => 10 ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['price']); } public function test_product_price_must_be_positive() { $response = $this->postJson('/api/products', [ 'name' => 'منتج تجريبي', 'price' => -50.00, 'stock' => 10 ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['price']); } public function test_product_stock_must_be_integer() { $response = $this->postJson('/api/products', [ 'name' => 'منتج تجريبي', 'price' => 99.99, 'stock' => 10.5 // يجب أن يكون عدد صحيح ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['stock']); } public function test_validates_all_fields_at_once() { $response = $this->postJson('/api/products', [ // جميع الحقول غير صالحة أو مفقودة ]); $response->assertUnprocessable() ->assertJsonValidationErrors([ 'name', 'price', 'stock', 'category_id' ]); }
اختبار التحقق بشكل مكثف: لكل حقل، اختبر: مطلوب/اختياري، نوع البيانات، القيم الدنيا/القصوى، أنماط التنسيق، وقيود الفرادة. هذا يضمن أن واجهة برمجة التطبيقات الخاصة بك ترفض البيانات غير الصالحة قبل وصولها إلى قاعدة البيانات.

اختبار المصادقة

تتطلب معظم واجهات برمجة التطبيقات المصادقة. يجعل Laravel Sanctum هذا سهلاً للاختبار:

<?php namespace Tests\Feature\Api; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\User; use App\Models\Product; class AuthenticationTest extends TestCase { use RefreshDatabase; public function test_unauthenticated_users_cannot_access_protected_routes() { $response = $this->getJson('/api/products'); $response->assertUnauthorized(); } public function test_authenticated_users_can_access_protected_routes() { $user = User::factory()->create(); $response = $this->actingAs($user) ->getJson('/api/products'); $response->assertOk(); } public function test_can_login_with_valid_credentials() { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123') ]); $response = $this->postJson('/api/login', [ 'email' => 'test@example.com', 'password' => 'password123' ]); $response->assertOk() ->assertJsonStructure([ 'token', 'user' => [ 'id', 'name', 'email' ] ]); } public function test_cannot_login_with_invalid_credentials() { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123') ]); $response = $this->postJson('/api/login', [ 'email' => 'test@example.com', 'password' => 'كلمة-مرور-خاطئة' ]); $response->assertUnauthorized(); } public function test_can_logout() { $user = User::factory()->create(); $response = $this->actingAs($user) ->postJson('/api/logout'); $response->assertNoContent(); } public function test_users_can_only_access_own_resources() { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $product = Product::factory()->create([ 'user_id' => $user1->id ]); // المستخدم 2 يحاول الوصول إلى منتج المستخدم 1 $response = $this->actingAs($user2) ->getJson("/api/products/{$product->id}"); $response->assertForbidden(); } }

لمصادقة رمز واجهة برمجة التطبيقات:

<?php public function test_can_access_api_with_token() { $user = User::factory()->create(); $token = $user->createToken('test-token'); $response = $this->withToken($token->plainTextToken) ->getJson('/api/products'); $response->assertOk(); } public function test_cannot_access_with_invalid_token() { $response = $this->withToken('رمز-غير-صالح') ->getJson('/api/products'); $response->assertUnauthorized(); }

تأكيدات قاعدة البيانات

يوفر Laravel العديد من تأكيدات قاعدة البيانات للتحقق من استمرارية البيانات:

<?php public function test_database_assertions() { $product = Product::factory()->create([ 'name' => 'منتج تجريبي', 'price' => 99.99 ]); // التأكد من وجود السجل في قاعدة البيانات $this->assertDatabaseHas('products', [ 'name' => 'منتج تجريبي', 'price' => 99.99 ]); // التأكد من أن السجل غير موجود $this->assertDatabaseMissing('products', [ 'name' => 'منتج غير موجود' ]); // التأكد من العدد الدقيق $this->assertDatabaseCount('products', 1); // حذف المنتج $product->delete(); // التأكد من الحذف الناعم (إذا كنت تستخدم الحذف الناعم) $this->assertSoftDeleted('products', [ 'id' => $product->id ]); // التأكد من وجود النموذج في قاعدة البيانات $this->assertModelExists($product); // التأكد من حذف النموذج $this->assertModelMissing($product); }

اختبار التصفح

عندما تُرجع واجهة برمجة التطبيقات نتائج مقسمة إلى صفحات، اختبر بنية التصفح:

<?php public function test_products_are_paginated() { // إنشاء 25 منتج Product::factory()->count(25)->create(); $response = $this->getJson('/api/products?page=1'); $response->assertOk() ->assertJsonStructure([ 'data', 'links' => [ 'first', 'last', 'prev', 'next' ], 'meta' => [ 'current_page', 'from', 'last_page', 'per_page', 'to', 'total' ] ]) ->assertJsonPath('meta.total', 25) ->assertJsonPath('meta.per_page', 15) ->assertJsonCount(15, 'data'); } public function test_can_navigate_pagination() { Product::factory()->count(25)->create(); // الصفحة الأولى $response = $this->getJson('/api/products?page=1'); $response->assertJsonPath('meta.current_page', 1); // الصفحة الثانية $response = $this->getJson('/api/products?page=2'); $response->assertJsonPath('meta.current_page', 2) ->assertJsonCount(10, 'data'); // العناصر المتبقية }

اختبار التصفية والفرز

اختبار أن معاملات الاستعلام تعمل بشكل صحيح:

<?php public function test_can_filter_products_by_category() { $category1 = Category::factory()->create(['name' => 'إلكترونيات']); $category2 = Category::factory()->create(['name' => 'كتب']); Product::factory()->count(3)->create(['category_id' => $category1->id]); Product::factory()->count(2)->create(['category_id' => $category2->id]); $response = $this->getJson("/api/products?category={$category1->id}"); $response->assertOk() ->assertJsonCount(3, 'data'); } public function test_can_search_products() { Product::factory()->create(['name' => 'حاسوب محمول للألعاب']); Product::factory()->create(['name' => 'حاسوب محمول للمكتب']); Product::factory()->create(['name' => 'حاسوب مكتبي']); $response = $this->getJson('/api/products?search=محمول'); $response->assertOk() ->assertJsonCount(2, 'data'); } public function test_can_sort_products() { Product::factory()->create(['name' => 'منتج ج', 'price' => 50]); Product::factory()->create(['name' => 'منتج أ', 'price' => 100]); Product::factory()->create(['name' => 'منتج ب', 'price' => 75]); // الفرز حسب السعر تصاعديًا $response = $this->getJson('/api/products?sort=price&order=asc'); $response->assertOk(); $data = $response->json('data'); $this->assertEquals(50, $data[0]['price']); $this->assertEquals(75, $data[1]['price']); $this->assertEquals(100, $data[2]['price']); }

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

قم بتنفيذ مجموعة الاختبارات باستخدام PHPUnit من خلال Artisan:

# تشغيل جميع الاختبارات php artisan test # تشغيل الاختبارات مع إخراج مفصل php artisan test --verbose # تشغيل ملف اختبار محدد php artisan test tests/Feature/Api/ProductTest.php # تشغيل طريقة اختبار محددة php artisan test --filter test_can_create_product # تشغيل الاختبارات بالتوازي (أسرع) php artisan test --parallel # إنشاء تقرير تغطية الكود php artisan test --coverage
الاختبار المستمر: قم بتشغيل الاختبارات بشكل متكرر أثناء التطوير. استخدم php artisan test --filter لتشغيل الاختبارات التي تعمل عليها فقط، ثم قم بتشغيل المجموعة الكاملة قبل تنفيذ التغييرات.
تمرين تطبيقي:
  1. أنشئ فئة اختبار tests/Feature/Api/OrderTest.php
  2. اكتب اختبارات لإنشاء طلب مع التحقق (customer_id مطلوب، عنصر واحد على الأقل، كميات صالحة)
  3. اختبر أن الطلبات يمكن عرضها فقط من قبل مالكها
  4. اختبر أن المسؤولين يمكنهم عرض جميع الطلبات
  5. اختبر انتقالات حالة الطلب (قيد الانتظار ← قيد المعالجة ← تم الشحن ← تم التسليم)
  6. اختبر أن الطلبات لا يمكن حذفها بمجرد شحنها
  7. اكتب حالات مصنع لحالات الطلب المختلفة
  8. اختبر تصفية الطلبات حسب الحالة ونطاق التاريخ

أفضل الممارسات لاختبار واجهة برمجة التطبيقات

  • الاختبار بشكل منعزل: يجب أن يكون كل اختبار مستقلاً ولا يعتمد على اختبارات أخرى. استخدم RefreshDatabase لضمان حالة نظيفة.
  • اختبار الحالات الحدية: لا تختبر فقط المسار السعيد. اختبر مع بيانات غير صالحة وحقول مفقودة وقيم حدودية وحالات حدية.
  • استخدم أسماء اختبار وصفية: قم بتسمية الاختبارات بوضوح حتى تكون الإخفاقات سهلة الفهم: test_cannot_create_product_with_negative_price
  • حافظ على سرعة الاختبارات: استخدم قواعد بيانات في الذاكرة، وتجنب استدعاءات HTTP غير الضرورية، واستخدم المصانع بكفاءة.
  • اختبار منطق الأعمال: ركز على اختبار ما تفعله واجهة برمجة التطبيقات الخاصة بك، وليس كيف تعمل الأطر. اختبر قواعد عملك الفريدة.
  • حافظ على تغطية الاختبار: استهدف تغطية عالية للمسارات الحرجة. استخدم --coverage لتحديد الكود غير المختبر.
  • اختبار الأمان: دائمًا اختبر المصادقة والترخيص والتحقق من المدخلات بشكل شامل.
  • استخدام تأكيدات ذات مغزى: تأكيدات محددة متعددة أفضل من تأكيد واحد عام.