الاختبارات و TDD

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

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

أساسيات اختبار رفع الملفات

يعد اختبار رفع الملفات أمراً ضرورياً للتطبيقات التي تتعامل مع محتوى ينشئه المستخدم مثل صور الملف الشخصي والمستندات ومقاطع الفيديو أو المرفقات. يتحقق اختبار رفع الملفات المناسب من أن الملفات يتم التحقق منها وتخزينها بشكل صحيح واسترجاعها بدقة وحذفها عند الاقتضاء—كل ذلك دون ملء بيئة الاختبار بملفات حقيقية.

مبدأ أساسي: لا تستخدم أبداً إدخال/إخراج ملفات حقيقي في الاختبارات. استخدم أنظمة تخزين وهمية تحاكي عمليات الملفات في الذاكرة. هذا يحافظ على الاختبارات سريعة ومعزولة ويمنع فوضى نظام الملفات بملفات الاختبار. Storage::fake() في Laravel والأدوات المماثلة تجعل هذا بسيطاً.

لماذا نختبر رفع الملفات؟

وظيفة رفع الملفات لها مخاوف فريدة من نوعها فيما يتعلق بالأمان والتحقق:

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

اختبار تخزين Laravel

توفر واجهة Storage في Laravel قرصاً وهمياً قوياً للاختبار:

إعداد التخزين الوهمي الأساسي

<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Tests\TestCase; class FileUploadTest extends TestCase { use RefreshDatabase; public function test_user_can_upload_avatar() { Storage::fake('public'); // إنشاء قرص وهمي $user = User::factory()->create(); $file = UploadedFile::fake()->image('avatar.jpg', 200, 200); $response = $this->actingAs($user) ->post('/profile/avatar', [ 'avatar' => $file ]); $response->assertOk(); // تأكيد تخزين الملف Storage::disk('public')->assertExists('avatars/' . $file->hashName()); // تأكيد تحديث قاعدة البيانات $this->assertDatabaseHas('users', [ 'id' => $user->id, 'avatar' => 'avatars/' . $file->hashName() ]); } }
فوائد Storage::fake():
  • ينشئ نظام ملفات في الذاكرة—لا توجد ملفات حقيقية مكتوبة على القرص
  • يتم تنظيفه تلقائياً بعد كل اختبار
  • يوفر تأكيدات: assertExists()، assertMissing()
  • يعمل مع جميع عمليات تخزين Laravel (put، get، delete، copy، move)
  • يمكن تزييف أقراص محددة: Storage::fake('s3')

إنشاء ملفات وهمية

<?php // صورة وهمية بأبعاد محددة $image = UploadedFile::fake()->image('photo.jpg', 1920, 1080); // صورة وهمية بحجم بالكيلوبايت $largeImage = UploadedFile::fake()->image('large.jpg')->size(5000); // 5 ميجابايت // مستند وهمي $pdf = UploadedFile::fake()->create('document.pdf', 1000); // 1 ميجابايت PDF // وهمي مع نوع MIME محدد $csv = UploadedFile::fake()->create('data.csv', 500, 'text/csv'); // ملفات متعددة $files = [ UploadedFile::fake()->image('photo1.jpg'), UploadedFile::fake()->image('photo2.jpg'), UploadedFile::fake()->image('photo3.jpg'), ];

اختبار التحقق من الملفات

اختبر أن تطبيقك يتحقق بشكل صحيح من الملفات المرفوعة:

التحقق من نوع الملف

<?php public function test_avatar_must_be_image() { Storage::fake('public'); $user = User::factory()->create(); $file = UploadedFile::fake()->create('document.pdf', 1000); $response = $this->actingAs($user) ->post('/profile/avatar', [ 'avatar' => $file ]); $response->assertSessionHasErrors('avatar'); Storage::disk('public')->assertMissing('avatars/' . $file->hashName()); } public function test_only_specific_image_types_allowed() { Storage::fake('public'); $user = User::factory()->create(); // مسموح: JPEG، PNG، GIF $jpeg = UploadedFile::fake()->image('photo.jpg'); $png = UploadedFile::fake()->image('photo.png'); $gif = UploadedFile::fake()->image('photo.gif'); // غير مسموح: BMP $bmp = UploadedFile::fake()->create('photo.bmp', 100, 'image/bmp'); $this->actingAs($user) ->post('/profile/avatar', ['avatar' => $jpeg]) ->assertOk(); $this->actingAs($user) ->post('/profile/avatar', ['avatar' => $bmp]) ->assertSessionHasErrors('avatar'); }

التحقق من حجم الملف

<?php public function test_avatar_cannot_exceed_max_size() { Storage::fake('public'); $user = User::factory()->create(); // ملف 3 ميجابايت (أكثر من حد 2 ميجابايت) $largeFile = UploadedFile::fake()->image('large.jpg')->size(3000); $response = $this->actingAs($user) ->post('/profile/avatar', [ 'avatar' => $largeFile ]); $response->assertSessionHasErrors('avatar'); } public function test_avatar_within_size_limit_accepted() { Storage::fake('public'); $user = User::factory()->create(); // ملف 1.5 ميجابايت (ضمن حد 2 ميجابايت) $validFile = UploadedFile::fake()->image('valid.jpg')->size(1500); $response = $this->actingAs($user) ->post('/profile/avatar', [ 'avatar' => $validFile ]); $response->assertOk(); Storage::disk('public')->assertExists('avatars/' . $validFile->hashName()); }

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

<?php public function test_avatar_must_meet_minimum_dimensions() { Storage::fake('public'); $user = User::factory()->create(); // صغير جداً (يتطلب حد أدنى 200x200) $tooSmall = UploadedFile::fake()->image('small.jpg', 100, 100); $response = $this->actingAs($user) ->post('/profile/avatar', [ 'avatar' => $tooSmall ]); $response->assertSessionHasErrors('avatar'); } public function test_banner_must_have_correct_aspect_ratio() { Storage::fake('public'); $user = User::factory()->create(); // نسبة عرض إلى ارتفاع خاطئة (يتطلب 16:9) $wrongRatio = UploadedFile::fake()->image('banner.jpg', 1000, 1000); // 1:1 $response = $this->actingAs($user) ->post('/profile/banner', [ 'banner' => $wrongRatio ]); $response->assertSessionHasErrors('banner'); // نسبة عرض إلى ارتفاع صحيحة $correctRatio = UploadedFile::fake()->image('banner.jpg', 1920, 1080); // 16:9 $response = $this->actingAs($user) ->post('/profile/banner', [ 'banner' => $correctRatio ]); $response->assertOk(); }

اختبار تخزين واسترجاع الملفات

اختبار مسارات التخزين

<?php public function test_files_stored_in_correct_directory() { Storage::fake('s3'); $post = Post::factory()->create(); $image = UploadedFile::fake()->image('cover.jpg'); $response = $this->actingAs($post->user) ->post("/posts/{$post->id}/cover", [ 'cover' => $image ]); // تأكيد تخزين الملف في المسار الصحيح Storage::disk('s3')->assertExists( 'posts/' . $post->id . '/' . $image->hashName() ); } public function test_files_organized_by_date() { Storage::fake('public'); $user = User::factory()->create(); $document = UploadedFile::fake()->create('report.pdf', 1000); $this->actingAs($user) ->post('/documents', [ 'file' => $document ]); $datePath = now()->format('Y/m/d'); Storage::disk('public')->assertExists( "documents/{$datePath}/" . $document->hashName() ); }

اختبار استرجاع الملفات

<?php public function test_user_can_download_own_document() { Storage::fake('private'); $user = User::factory()->create(); $content = 'محتوى المستند الاختباري'; Storage::disk('private')->put( 'documents/test.pdf', $content ); $document = Document::create([ 'user_id' => $user->id, 'filename' => 'test.pdf', 'path' => 'documents/test.pdf' ]); $response = $this->actingAs($user) ->get("/documents/{$document->id}/download"); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); $this->assertEquals($content, $response->getContent()); } public function test_user_cannot_download_other_users_document() { Storage::fake('private'); $owner = User::factory()->create(); $otherUser = User::factory()->create(); $document = Document::factory()->for($owner)->create(); $response = $this->actingAs($otherUser) ->get("/documents/{$document->id}/download"); $response->assertForbidden(); }

اختبار حذف الملفات

<?php public function test_deleting_post_deletes_associated_images() { Storage::fake('public'); $post = Post::factory()->create(); $image = UploadedFile::fake()->image('cover.jpg'); // رفع صورة $this->actingAs($post->user) ->post("/posts/{$post->id}/cover", [ 'cover' => $image ]); $imagePath = $post->fresh()->cover_image; Storage::disk('public')->assertExists($imagePath); // حذف المنشور $this->actingAs($post->user) ->delete("/posts/{$post->id}"); // تأكيد حذف الصورة Storage::disk('public')->assertMissing($imagePath); } public function test_replacing_avatar_deletes_old_file() { Storage::fake('public'); $user = User::factory()->create(); // رفع الصورة الرمزية الأولى $oldAvatar = UploadedFile::fake()->image('old.jpg'); $this->actingAs($user) ->post('/profile/avatar', ['avatar' => $oldAvatar]); $oldPath = $user->fresh()->avatar; Storage::disk('public')->assertExists($oldPath); // رفع صورة رمزية جديدة $newAvatar = UploadedFile::fake()->image('new.jpg'); $this->actingAs($user) ->post('/profile/avatar', ['avatar' => $newAvatar]); $newPath = $user->fresh()->avatar; // تأكيد حذف الملف القديم، وجود الملف الجديد Storage::disk('public')->assertMissing($oldPath); Storage::disk('public')->assertExists($newPath); }

اختبار رفع ملفات متعددة

<?php public function test_can_upload_multiple_images() { Storage::fake('public'); $post = Post::factory()->create(); $images = [ UploadedFile::fake()->image('photo1.jpg'), UploadedFile::fake()->image('photo2.jpg'), UploadedFile::fake()->image('photo3.jpg'), ]; $response = $this->actingAs($post->user) ->post("/posts/{$post->id}/gallery", [ 'images' => $images ]); $response->assertOk(); // تأكيد تخزين جميع الصور foreach ($images as $image) { Storage::disk('public')->assertExists( 'gallery/' . $image->hashName() ); } // تأكيد إنشاء سجلات قاعدة البيانات $this->assertDatabaseCount('post_images', 3); } public function test_validates_all_files_in_batch_upload() { Storage::fake('public'); $post = Post::factory()->create(); $files = [ UploadedFile::fake()->image('photo1.jpg'), UploadedFile::fake()->create('virus.exe', 100), // غير صالح UploadedFile::fake()->image('photo3.jpg'), ]; $response = $this->actingAs($post->user) ->post("/posts/{$post->id}/gallery", [ 'images' => $files ]); $response->assertSessionHasErrors('images.1'); // تأكيد عدم تخزين ملفات بسبب فشل التحقق Storage::disk('public')->assertMissing('gallery/' . $files[0]->hashName()); Storage::disk('public')->assertMissing('gallery/' . $files[2]->hashName()); }

اختبار تخزين الملفات بأقراص مختلفة

<?php public function test_public_files_stored_on_public_disk() { Storage::fake('public'); $post = Post::factory()->create(); $cover = UploadedFile::fake()->image('cover.jpg'); $this->actingAs($post->user) ->post("/posts/{$post->id}/cover", [ 'cover' => $cover ]); Storage::disk('public')->assertExists( 'covers/' . $cover->hashName() ); } public function test_private_documents_stored_on_private_disk() { Storage::fake('private'); $user = User::factory()->create(); $document = UploadedFile::fake()->create('confidential.pdf', 1000); $this->actingAs($user) ->post('/documents', [ 'file' => $document ]); Storage::disk('private')->assertExists( 'documents/' . $document->hashName() ); } public function test_large_files_stored_on_s3() { Storage::fake('s3'); $user = User::factory()->create(); $video = UploadedFile::fake()->create('video.mp4', 50000); // 50 ميجابايت $this->actingAs($user) ->post('/videos', [ 'video' => $video ]); Storage::disk('s3')->assertExists( 'videos/' . $video->hashName() ); }

اختبار رفع ملفات JavaScript

اختبر رفع الملفات في تطبيقات JavaScript:

// tests/components/FileUpload.test.jsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { FileUploadForm } from '../FileUploadForm'; import { uploadFile } from '../../services/api'; jest.mock('../../services/api'); describe('FileUploadForm', () => { test('uploads file successfully', async () => { uploadFile.mockResolvedValue({ success: true, filename: 'avatar.jpg', url: '/storage/avatars/avatar.jpg' }); render(<FileUploadForm />); const file = new File(['avatar'], 'avatar.jpg', { type: 'image/jpeg' }); const input = screen.getByLabelText('Upload Avatar'); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(uploadFile).toHaveBeenCalledWith(file); expect(screen.getByText('Upload successful')).toBeInTheDocument(); }); }); test('shows error for invalid file type', async () => { render(<FileUploadForm />); const file = new File(['document'], 'document.pdf', { type: 'application/pdf' }); const input = screen.getByLabelText('Upload Avatar'); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(screen.getByText('Only images allowed')).toBeInTheDocument(); }); }); test('shows error for file too large', async () => { render(<FileUploadForm maxSize={2 * 1024 * 1024} />); // 2 ميجابايت // إنشاء ملف 3 ميجابايت const largeFile = new File( [new ArrayBuffer(3 * 1024 * 1024)], 'large.jpg', { type: 'image/jpeg' } ); const input = screen.getByLabelText('Upload Avatar'); fireEvent.change(input, { target: { files: [largeFile] } }); await waitFor(() => { expect(screen.getByText('File size exceeds 2MB')).toBeInTheDocument(); }); }); test('displays upload progress', async () => { uploadFile.mockImplementation(() => { return new Promise((resolve) => { setTimeout(() => resolve({ success: true }), 1000); }); }); render(<FileUploadForm />); const file = new File(['avatar'], 'avatar.jpg', { type: 'image/jpeg' }); const input = screen.getByLabelText('Upload Avatar'); fireEvent.change(input, { target: { files: [file] } }); expect(screen.getByText('Uploading...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Upload successful')).toBeInTheDocument(); }); }); });

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

1. استخدم دائماً التخزين الوهمي

لا تكتب أبداً ملفات حقيقية أثناء الاختبارات. استخدم Storage::fake() في Laravel أو أنظمة ملفات وهمية في JavaScript.

2. اختبر جميع قواعد التحقق

اختبر نوع الملف والحجم والأبعاد وأي تحقق مخصص. قم بتضمين الحالات الصالحة وغير الصالحة.

3. اختبر تنظيف الملفات

تحقق من أن حذف السجلات يحذف أيضاً الملفات المرتبطة لمنع الملفات اليتيمة.

4. اختبر التفويض

تأكد من أن المستخدمين يمكنهم فقط رفع/تنزيل/حذف ملفاتهم الخاصة أو الملفات التي لديهم إذن للوصول إليها.

5. اختبر معالجة الأخطاء

محاكاة فشل التحميل وسيناريوهات التخزين الممتلئ وأخطاء الشبكة.

6. احتفظ بملفات الاختبار صغيرة

استخدم ملفات وهمية صغيرة (1-10 كيلوبايت) للسرعة. استخدم فقط ملفات أكبر عند اختبار حدود الحجم.

قائمة التحقق من اختبار الأمان:
  • التحقق من نوع الملف: حظر الملفات القابلة للتنفيذ (.exe، .sh، .php)
  • التحقق من نوع MIME: لا تثق في أنواع MIME المقدمة من العميل
  • تنقية اسم الملف: منع اجتياز المسار (../../etc/passwd)
  • حدود الحجم: منع هجمات DoS عبر التحميلات الضخمة
  • فحص الفيروسات: للإنتاج، دمج فحص مكافحة الفيروسات
  • الوصول العام: تحقق من أن الملفات الخاصة ليست متاحة للعامة

تمرين عملي

التمرين 1: اختبار نظام إدارة المستندات

<?php // الميزات للاختبار: // 1. يمكن للمستخدم رفع مستندات PDF (حد أقصى 10 ميجابايت) // 2. المستندات منظمة حسب user_id/year/month/ // 3. يمكن للمستخدم تنزيل مستنداته الخاصة // 4. لا يمكن للمستخدم تنزيل مستندات مستخدمين آخرين // 5. يمكن للمسؤول تنزيل أي مستند // 6. حذف مستند يحذف الملف // 7. فقط PDF، DOC، DOCX مسموح // 8. اسم الملف مخزن في قاعدة البيانات مع البيانات الوصفية // اكتب مجموعة اختبار شاملة

التمرين 2: اختبار معرض صور منتج متعدد الصور

// الميزات للاختبار: // 1. رفع ما يصل إلى 5 صور لكل منتج // 2. الصورة الأولى أساسية، والأخرى معرض // 3. الصور يجب أن تكون JPEG/PNG، حد أدنى 800x600 بكسل // 4. إعادة ترتيب الصور يحدث ترتيب العرض // 5. حذف المنتج يحذف جميع الصور // 6. استبدال الصورة الأساسية يحذف القديمة // 7. إنشاء صور مصغرة (اختبار مخزن بشكل منفصل) // اكتب اختبارات مع Storage::fake()

التمرين 3: اختبار رفع الصورة الرمزية مع معالجة الصور

// الميزات للاختبار: // 1. رفع صورة رمزية (JPEG/PNG، حد أقصى 2 ميجابايت) // 2. تغيير الحجم إلى 200x200 بكسل (اختبار الأبعاد) // 3. إنشاء صورة مصغرة 50x50 بكسل // 4. تخزين الأصلية والصورة المصغرة // 5. الصور الرمزية القديمة محذوفة عند الاستبدال // 6. الصورة الرمزية الافتراضية تُستخدم إذا لم يتم رفع أي شيء // 7. عنوان URL للصورة الرمزية يُرجع في استجابة API // تلميح: استخدم Intervention Image أو ما شابه // محاكاة معالجة الصور في الاختبارات