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

الغوص العميق في التأكيدات: إتقان التحقق من الاختبار

25 دقيقة الدرس 6 من 35

فهم التأكيدات

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

توفر أطر الاختبار المختلفة واجهات برمجية مختلفة للتأكيدات، لكن المفاهيم الأساسية تظل كما هي: أنت تدعي كيف يجب أن يتصرف الكود الخاص بك، وإطار الاختبار يتحقق من هذا الادعاء. في هذا الدرس، سنستكشف أنماط التأكيد في كل من PHPUnit (لـ PHP) وJest (لـ JavaScript)، مع فهم متى وكيف نستخدم كل نوع من التأكيدات بفعالية.

عقلية التأكيد أولاً:

قبل كتابة أي كود، فكر في ما تريد التأكيد عليه. ما السلوك الذي تختبره؟ ما الذي يجب أن يكون عليه الناتج؟ هذا التفكير "التأكيد أولاً" يساعدك على كتابة اختبارات أكثر تركيزاً وكود أوضح.

تأكيدات PHPUnit

تأكيدات المساواة الأساسية

يوفر PHPUnit طرقاً متعددة لاختبار المساواة، كل منها مناسب لسيناريوهات مختلفة:

// المساواة الصارمة (مقارنة ===) $this->assertSame(5, $calculator->add(2, 3)); $this->assertSame('hello', $greeter->greet()); // المساواة المرنة (مقارنة ==) $this->assertEquals(5, '5'); // ينجح - تحويل النوع $this->assertEquals([1, 2], ['1', '2']); // ينجح - مقارنة مرنة // ليس متساوياً $this->assertNotSame(5, 6); $this->assertNotEquals('hello', 'world');
المقارنات الصارمة مقابل المرنة:

استخدم assertSame() للتحقق الصارم من النوع. استخدم assertEquals() فقط عندما تريد عمداً تحويل النوع. في معظم الحالات، المقارنات الصارمة تكتشف المزيد من الأخطاء وتجعل اختباراتك أكثر موثوقية.

تأكيدات القيم المنطقية وNull

// تأكيدات القيم المنطقية $this->assertTrue($user->isActive()); $this->assertFalse($user->isDeleted()); // تأكيدات Null $this->assertNull($user->getDeletedAt()); $this->assertNotNull($user->getCreatedAt());

تأكيدات المصفوفات والمجموعات

يوفر PHPUnit تأكيدات قوية للعمل مع المصفوفات والمجموعات:

$users = $repository->findAll(); // التحقق من حجم المصفوفة $this->assertCount(10, $users); // التحقق من احتواء المصفوفة على قيمة $this->assertContains('admin@example.com', $emails); // التحقق من وجود مفتاح محدد في المصفوفة $this->assertArrayHasKey('email', $user); // التحقق من كون المصفوفة فارغة $this->assertEmpty($errors); $this->assertNotEmpty($results); // التحقق من كون المصفوفة مجموعة فرعية من أخرى $expected = ['name' => 'John', 'active' => true]; $this->assertArraySubset($expected, $user);

تأكيدات النصوص

// التحقق من احتواء النص على سلسلة فرعية $this->assertStringContainsString('error', $message); // التحقق من بداية/نهاية النص $this->assertStringStartsWith('https://', $url); $this->assertStringEndsWith('.com', $domain); // المطابقة باستخدام التعبيرات النمطية $this->assertMatchesRegularExpression('/^[A-Z]\w+$/', $className); // تأكيدات نصوص JSON $this->assertJson($response->getContent()); $this->assertJsonStringEqualsJsonString( '{"name":"John"}', $response->getContent() );

تأكيدات الأنواع

// التحقق من أنواع المتغيرات $this->assertIsInt($user->getId()); $this->assertIsString($user->getName()); $this->assertIsArray($user->getRoles()); $this->assertIsBool($user->isActive()); $this->assertIsFloat($product->getPrice()); $this->assertIsObject($user); // التحقق من أنواع الكائنات $this->assertInstanceOf(User::class, $user); $this->assertInstanceOf(Collection::class, $users);

تأكيدات الاستثناءات

// توقع رفع استثناء $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Email cannot be empty'); $this->expectExceptionCode(400); $user->setEmail(''); // يجب أن يرفع استثناءً // صيغة بديلة لاختبار الاستثناءات try { $user->setEmail(''); $this->fail('Expected exception was not thrown'); } catch (InvalidArgumentException $e) { $this->assertStringContainsString('Email', $e->getMessage()); }
أفضل ممارسات اختبار الاستثناءات:

تحقق دائماً من نوع الاستثناء ورسالته عند اختبار الاستثناءات. هذا يضمن أنك تمسك بالخطأ الصحيح للسبب الصحيح، وليس أي استثناء.

تأكيدات الملفات والمجلدات

// وجود الملف $this->assertFileExists('/path/to/file.txt'); $this->assertFileDoesNotExist('/path/to/deleted.txt'); // وجود المجلد $this->assertDirectoryExists('/path/to/uploads'); $this->assertDirectoryIsReadable('/path/to/logs'); $this->assertDirectoryIsWritable('/path/to/cache'); // محتوى الملف $this->assertFileEquals( '/path/to/expected.txt', '/path/to/actual.txt' ); $this->assertStringEqualsFile( '/path/to/expected.json', json_encode($data) );

مطابقات Jest

المطابقات الأساسية

يستخدم Jest "matchers" بدلاً من "assertions" - المصطلح مختلف، لكن المفهوم هو نفسه:

// المساواة الدقيقة (Object.is) expect(2 + 2).toBe(4); expect(user.name).toBe('John'); // المساواة العميقة (للكائنات والمصفوفات) expect(user).toEqual({ name: 'John', email: 'john@example.com', active: true }); expect([1, 2, 3]).toEqual([1, 2, 3]); // ليس متساوياً expect(5).not.toBe(3); expect('hello').not.toEqual('world');
toBe مقابل toEqual:

استخدم toBe() للقيم البدائية وtoEqual() للكائنات/المصفوفات. toBe() يستخدم Object.is() الذي يقارن المراجع للكائنات، بينما toEqual() يقوم بمقارنة عميقة للقيم.

مطابقات الصحة

// قيم محددة expect(null).toBeNull(); expect(undefined).toBeUndefined(); expect(value).toBeDefined(); // Truthy/Falsy expect(true).toBeTruthy(); expect(1).toBeTruthy(); expect('hello').toBeTruthy(); expect(false).toBeFalsy(); expect(0).toBeFalsy(); expect('').toBeFalsy(); expect(null).toBeFalsy(); expect(undefined).toBeFalsy();

مطابقات الأرقام

// مطابقات المقارنة expect(10).toBeGreaterThan(5); expect(10).toBeGreaterThanOrEqual(10); expect(5).toBeLessThan(10); expect(5).toBeLessThanOrEqual(5); // مساواة الفاصلة العائمة expect(0.1 + 0.2).toBeCloseTo(0.3); // يتعامل مع دقة الفاصلة العائمة expect(Math.PI).toBeCloseTo(3.14159, 5); // 5 منازل عشرية // التحقق من كون الرقم NaN expect(NaN).toBeNaN(); expect(0 / 0).toBeNaN();

مطابقات النصوص

// مطابقة السلسلة الفرعية expect('Hello World').toContain('World'); expect('team').not.toContain('I'); // المطابقة بالتعبيرات النمطية expect('user@example.com').toMatch(/^[\w\.\-]+@[\w\.\-]+\.\w+$/); expect('Hello123').toMatch(/\d+/); // الطول expect('hello').toHaveLength(5); expect('').toHaveLength(0);

مطابقات المصفوفات والكائنات

const users = [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, { id: 3, name: 'Bob' } ]; // المصفوفة تحتوي على عنصر expect(users).toContainEqual({ id: 1, name: 'John' }); expect([1, 2, 3]).toContain(2); // طول المصفوفة expect(users).toHaveLength(3); expect([]).toHaveLength(0); // الكائن له خاصية expect(users[0]).toHaveProperty('name'); expect(users[0]).toHaveProperty('name', 'John'); expect(users[0]).toHaveProperty(['name'], 'John'); // مطابقة بنية الكائن expect(users[0]).toMatchObject({ id: 1, name: 'John' });

مطابقات الاستثناءات

// الدالة ترفع خطأ expect(() => { throw new Error('Invalid email'); }).toThrow(); // رسالة خطأ محددة expect(() => { throw new Error('Invalid email'); }).toThrow('Invalid email'); // نوع الخطأ expect(() => { throw new TypeError('Expected string'); }).toThrow(TypeError); // مطابقة رسالة الخطأ بالتعبير النمطي expect(() => { throw new Error('Email cannot be empty'); }).toThrow(/email/i);
خطأ شائع:

عند اختبار الاستثناءات، لف الكود دائماً في دالة سهمية. لا تستدعي الدالة مباشرة: expect(myFunction).toThrow() خطأ! استخدم: expect(() => myFunction()).toThrow()

مطابقات Promise

// Promise يتم حله test('promise resolves with user', async () => { await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'John' }); }); // Promise يرفض test('promise rejects with error', async () => { await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID'); }); // صيغة بديلة test('promise resolves', () => { return expect(fetchUser(1)).resolves.toBeDefined(); });

التأكيدات والمطابقات المخصصة

إنشاء تأكيدات PHPUnit مخصصة

يمكنك توسيع PHPUnit بطرق تأكيد مخصصة:

trait CustomAssertions { public function assertValidEmail(string $email, string $message = '') { $this->assertMatchesRegularExpression( '/^[\w\.\-]+@[\w\.\-]+\.\w+$/', $email, $message ?: "Failed asserting that '$email' is a valid email" ); } public function assertJsonStructure(array $structure, string $json) { $data = json_decode($json, true); foreach ($structure as $key) { $this->assertArrayHasKey( $key, $data, "JSON does not contain key: $key" ); } } public function assertDateFormat(string $date, string $format = 'Y-m-d') { $parsed = \DateTime::createFromFormat($format, $date); $this->assertNotFalse( $parsed, "Failed asserting that '$date' matches format '$format'" ); } } // الاستخدام في الاختبار class UserTest extends TestCase { use CustomAssertions; public function test_user_has_valid_email() { $user = new User('john@example.com'); $this->assertValidEmail($user->getEmail()); } }

إنشاء مطابقات Jest مخصصة

// تعريف مطابقات مخصصة expect.extend({ toBeValidEmail(received) { const emailRegex = /^[\w\.\-]+@[\w\.\-]+\.\w+$/; const pass = emailRegex.test(received); return { pass, message: () => pass ? `expected ${received} not to be a valid email` : `expected ${received} to be a valid email` }; }, toHaveStatus(response, expectedStatus) { const pass = response.status === expectedStatus; return { pass, message: () => pass ? `expected status not to be ${expectedStatus}` : `expected status ${response.status} to be ${expectedStatus}` }; }, toBeWithinRange(received, floor, ceiling) { const pass = received >= floor && received <= ceiling; return { pass, message: () => pass ? `expected ${received} not to be within range ${floor} - ${ceiling}` : `expected ${received} to be within range ${floor} - ${ceiling}` }; } }); // الاستخدام في الاختبارات test('validates email format', () => { expect('user@example.com').toBeValidEmail(); expect('invalid-email').not.toBeValidEmail(); }); test('response has correct status', async () => { const response = await fetch('/api/users'); expect(response).toHaveStatus(200); }); test('age is within valid range', () => { const user = { age: 25 }; expect(user.age).toBeWithinRange(18, 100); });
متى تنشئ مطابقات مخصصة:

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

أفضل ممارسات التأكيد

1. تأكيد منطقي واحد لكل اختبار

يجب أن يتحقق كل اختبار من سلوك محدد واحد:

// جيد - اختبار مركز test('user registration creates new user', () => { const user = registerUser('john@example.com', 'password123'); expect(user).toBeDefined(); expect(user.email).toBe('john@example.com'); }); // أفضل - تقسيم إلى اختبارات متعددة test('user registration returns user object', () => { const user = registerUser('john@example.com', 'password123'); expect(user).toBeDefined(); }); test('registered user has correct email', () => { const user = registerUser('john@example.com', 'password123'); expect(user.email).toBe('john@example.com'); });

2. استخدم رسائل تأكيد وصفية

// PHPUnit - معامل رسالة اختياري $this->assertTrue( $user->isActive(), 'Expected user to be active after registration' ); $this->assertCount( 10, $results, 'Expected to find 10 users, but found ' . count($results) ); // Jest - رسائل مخصصة عبر أوصاف الاختبار test('user should be active after registration', () => { expect(user.isActive()).toBe(true); });

3. اختبر حالات النجاح والفشل

describe('Email validation', () => { test('accepts valid email formats', () => { expect(isValidEmail('user@example.com')).toBe(true); expect(isValidEmail('user.name@example.co.uk')).toBe(true); expect(isValidEmail('user+tag@example.com')).toBe(true); }); test('rejects invalid email formats', () => { expect(isValidEmail('invalid')).toBe(false); expect(isValidEmail('@example.com')).toBe(false); expect(isValidEmail('user@')).toBe(false); expect(isValidEmail('')).toBe(false); }); });

4. استخدم تأكيدات محددة

// سيء - عام جداً expect(user.roles.length > 0).toBe(true); // جيد - تأكيد محدد expect(user.roles).not.toHaveLength(0); // أفضل - أكثر تحديداً expect(user.roles).toContain('admin'); // سيء - غامض expect(!!user.deletedAt).toBe(false); // جيد - نية واضحة expect(user.deletedAt).toBeNull();

5. تجنب المنطق في التأكيدات

// سيء - منطق في التأكيد expect(users.filter(u => u.active).length).toBe(5); // جيد - أعد القيمة أولاً const activeUsers = users.filter(u => u.active); expect(activeUsers).toHaveLength(5); // سيء - حساب في التأكيد expect(cart.items.reduce((sum, item) => sum + item.price, 0)).toBe(100); // جيد - احسب أولاً const totalPrice = cart.items.reduce((sum, item) => sum + item.price, 0); expect(totalPrice).toBe(100);
تمرين تطبيقي:
  1. أنشئ فئة Validator مع طرق للتحقق من البريد الإلكتروني والهاتف وURL
  2. اكتب اختبارات شاملة باستخدام أنواع التأكيد المختلفة:
    • تأكيدات منطقية لنتائج التحقق
    • تأكيدات نصية للمخرجات المنسقة
    • تأكيدات استثناءات للمدخلات غير الصالحة
  3. أنشئ تأكيداً/مطابقاً مخصصاً: toBeValidPhoneNumber()
  4. اكتب اختبارات تتحقق من الحالات الإيجابية والسلبية
  5. تأكد من أن كل اختبار له تأكيد واضح ومركز

الأنماط المضادة الشائعة للتأكيد

النمط المضاد 1: اختبار كود إطار العمل

// سيء - اختبار JavaScript نفسه test('array push works', () => { const arr = [1, 2]; arr.push(3); expect(arr).toHaveLength(3); }); // جيد - اختبار الكود الخاص بك test('addItem adds product to cart', () => { cart.addItem(product); expect(cart.items).toContain(product); });

النمط المضاد 2: تأكيدات لا معنى لها

// سيء - ينجح دائماً test('user exists', () => { const user = { name: 'John' }; expect(user).toBeDefined(); // سينجح هذا دائماً }); // جيد - يختبر السلوك الفعلي test('finds user by ID', () => { const user = findUserById(1); expect(user).toMatchObject({ id: 1, name: 'John' }); });

النمط المضاد 3: تأكيدات كثيرة جداً

// سيء - اختبار الكثير test('user creation', () => { const user = createUser(data); expect(user.id).toBeDefined(); expect(user.name).toBe('John'); expect(user.email).toBe('john@example.com'); expect(user.isActive).toBe(true); expect(user.createdAt).toBeInstanceOf(Date); expect(user.roles).toContain('user'); // ... المزيد من التأكيدات }); // جيد - اختبارات مركزة test('assigns ID to new user', () => { const user = createUser(data); expect(user.id).toBeDefined(); }); test('sets user properties from input', () => { const user = createUser({ name: 'John', email: 'john@example.com' }); expect(user).toMatchObject({ name: 'John', email: 'john@example.com' }); });

الخلاصة

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

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