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

أساسيات اختبار الوحدة

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

أساسيات اختبار الوحدة

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

ما هي الوحدة؟

"الوحدة" هي أصغر قطعة من الكود يمكن عزلها منطقيًا واختبارها بشكل مستقل. يمكن أن يختلف التعريف:

  • مستوى الوظيفة/الطريقة: اختبار الوظائف أو الطرق الفردية
  • مستوى الفئة: اختبار فئة مع جميع طرقها
  • مستوى الوحدة: اختبار وحدة أو مكون صغير

المبدأ الرئيسي: يجب أن يختبر اختبار الوحدة وحدة واحدة من الوظائف بمعزل عن الآخرين. إذا كان اختبارك يتطلب قاعدة بيانات أو استدعاءات API أو عمليات نظام الملفات، فمن المحتمل أن يكون اختبار تكامل، وليس اختبار وحدة.

خصائص اختبارات الوحدة الجيدة

تشترك اختبارات الوحدة الفعالة في هذه الخصائص:

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

نمط AAA

نمط AAA (الترتيب-التنفيذ-التأكيد) هو البنية الأكثر شهرة لكتابة اختبارات الوحدة. يقسم الاختبارات إلى ثلاثة أقسام واضحة:

1. الترتيب (Arrange - الإعداد)

إعداد شروط الاختبار وتهيئة الكائنات اللازمة للاختبار.

2. التنفيذ (Act - التشغيل)

تنفيذ الكود المراد اختباره—استدعاء الوظيفة أو الطريقة.

3. التأكيد (Assert - التحقق)

التحقق من أن الكود تصرف كما هو متوقع.

<script> // مثال JavaScript باستخدام نمط AAA test('calculateTotal should sum prices correctly', () => { // الترتيب: إعداد بيانات الاختبار const items = [ { price: 10.00, quantity: 2 }, { price: 5.50, quantity: 3 } ]; // التنفيذ: تنفيذ الوظيفة const total = calculateTotal(items); // التأكيد: التحقق من النتيجة expect(total).toBe(36.50); }); </script>
<?php // مثال PHP باستخدام نمط AAA public function test_user_full_name_concatenation() { // الترتيب $user = new User(); $user->first_name = 'John'; $user->last_name = 'Doe'; // التنفيذ $fullName = $user->getFullName(); // التأكيد $this->assertEquals('John Doe', $fullName); } ?>

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

تشريح اختبار الوحدة

دعنا نحلل بنية اختبار وحدة مكتوب جيدًا:

اسم الاختبار

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

<script> // جيد: وصفي وواضح test('login returns token when credentials are valid', () => { }); // سيء: غامض وغير واضح test('test1', () => { }); test('it works', () => { }); </script>

جسم الاختبار

اتبع نمط AAA لهيكلة منطق الاختبار بوضوح.

التأكيدات

استخدم التأكيدات المناسبة التي تجعل فشل الاختبار سهل الفهم.

كتابة أول اختبار وحدة

دعنا نكتب اختبار وحدة كامل لوظيفة آلة حاسبة بسيطة:

<script> // calculator.js - الكود الذي نريد اختباره function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } function multiply(a, b) { return a * b; } function divide(a, b) { if (b === 0) { throw new Error('Division by zero'); } return a / b; } module.exports = { add, subtract, multiply, divide }; </script>
<script> // calculator.test.js - اختبارات الوحدة الخاصة بنا const { add, subtract, multiply, divide } = require('./calculator'); describe('Calculator', () => { describe('add', () => { test('should add two positive numbers', () => { // الترتيب const a = 5; const b = 3; // التنفيذ const result = add(a, b); // التأكيد expect(result).toBe(8); }); test('should add negative numbers', () => { expect(add(-5, -3)).toBe(-8); }); test('should add zero', () => { expect(add(5, 0)).toBe(5); }); }); describe('divide', () => { test('should divide numbers correctly', () => { expect(divide(10, 2)).toBe(5); }); test('should throw error when dividing by zero', () => { expect(() => divide(10, 0)).toThrow('Division by zero'); }); }); }); </script>

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

نظم الاختبارات بشكل منطقي لتسهيل التنقل والفهم:

كتل describe

جمع الاختبارات ذات الصلة معًا باستخدام كتل describe.

<script> describe('User', () => { describe('authentication', () => { test('should login with valid credentials', () => { }); test('should reject invalid password', () => { }); }); describe('profile', () => { test('should update profile information', () => { }); test('should validate email format', () => { }); }); }); </script>

اختبار سيناريوهات مختلفة

تغطي اختبارات الوحدة الشاملة سيناريوهات متعددة:

المسار السعيد (الحالة الطبيعية)

اختبار السلوك المتوقع مع المدخلات الصالحة.

<script> test('should create user with valid data', () => { const user = createUser('John', 'john@example.com'); expect(user.name).toBe('John'); expect(user.email).toBe('john@example.com'); }); </script>

الحالات الحدية

اختبار الشروط الحدودية والمدخلات غير العادية ولكن الصالحة.

<script> test('should handle empty string', () => { expect(trimString('')).toBe(''); }); test('should handle very long strings', () => { const longString = 'a'.repeat(10000); expect(processString(longString).length).toBe(10000); }); </script>

حالات الخطأ

اختبار كيفية تعامل الكود الخاص بك مع المدخلات غير الصالحة وشروط الخطأ.

<script> test('should throw error for null input', () => { expect(() => processUser(null)).toThrow(); }); test('should return error message for invalid email', () => { const result = validateEmail('invalid-email'); expect(result.isValid).toBe(false); expect(result.error).toBe('Invalid email format'); }); </script>

خطأ شائع: اختبار المسار السعيد فقط. اختبر دائمًا الحالات الحدية وشروط الخطأ—هنا حيث غالبًا ما تختبئ الأخطاء!

عزل الاختبار

يجب عزل اختبارات الوحدة عن الاعتمادات الخارجية وعن بعضها البعض:

لا اعتمادات خارجية

لا تعتمد على قواعد البيانات أو واجهات برمجة التطبيقات أو أنظمة الملفات أو استدعاءات الشبكة.

<script> // سيء: هذا ليس اختبار وحدة—يعتمد على قاعدة بيانات test('should save user to database', async () => { await database.connect(); const user = await User.create({ name: 'John' }); expect(user.id).toBeDefined(); }); // جيد: هذا اختبار وحدة—يختبر المنطق بمعزل test('should validate user data before saving', () => { const user = new User({ name: 'John' }); expect(user.isValid()).toBe(true); }); </script>

اختبارات مستقلة

يجب أن يعمل كل اختبار بشكل مستقل—لا يجب أن يؤثر على بعضهم البعض.

<script> // سيء: الاختبارات تعتمد على حالة مشتركة let counter = 0; test('first test', () => { counter++; expect(counter).toBe(1); }); test('second test', () => { counter++; // يعتمد على الاختبار الأول! expect(counter).toBe(2); }); // جيد: كل اختبار مستقل test('first test', () => { let counter = 0; counter++; expect(counter).toBe(1); }); test('second test', () => { let counter = 0; counter++; expect(counter).toBe(1); }); </script>

الإعداد والتنظيف

استخدم خطافات الإعداد والتنظيف لتحضير وتنظيف بيئات الاختبار:

<script> describe('ShoppingCart', () => { let cart; // يعمل قبل كل اختبار beforeEach(() => { cart = new ShoppingCart(); }); // يعمل بعد كل اختبار afterEach(() => { cart = null; }); test('should start empty', () => { expect(cart.items).toHaveLength(0); }); test('should add item', () => { cart.addItem({ id: 1, name: 'Product' }); expect(cart.items).toHaveLength(1); }); }); </script>

الخطافات المتاحة:

  • beforeAll() - يعمل مرة واحدة قبل جميع الاختبارات في كتلة describe
  • beforeEach() - يعمل قبل كل اختبار
  • afterEach() - يعمل بعد كل اختبار
  • afterAll() - يعمل مرة واحدة بعد جميع الاختبارات في كتلة describe

اختبار الوظائف النقية مقابل غير النقية

الوظائف النقية (سهلة الاختبار)

الوظائف النقية تعيد دائمًا نفس الإخراج لنفس الإدخال وليس لها آثار جانبية.

<script> // وظيفة نقية - سهلة الاختبار function calculateDiscount(price, percentage) { return price * (percentage / 100); } test('should calculate discount correctly', () => { expect(calculateDiscount(100, 10)).toBe(10); expect(calculateDiscount(100, 10)).toBe(10); // دائمًا نفس النتيجة }); </script>

الوظائف غير النقية (تتطلب محاكاة)

الوظائف غير النقية لها آثار جانبية أو تعتمد على حالة خارجية—تحتاج إلى معالجة خاصة.

<script> // وظيفة غير نقية - أصعب في الاختبار function saveUser(user) { const timestamp = new Date(); // اعتماد خارجي user.createdAt = timestamp; database.save(user); // أثر جانبي return user; } // الاختبار يتطلب محاكاة test('should set createdAt timestamp', () => { const mockDate = new Date('2024-01-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const user = saveUser({ name: 'John' }); expect(user.createdAt).toEqual(mockDate); }); </script>

ما لا يجب اختباره

ليس كل شيء يحتاج إلى اختبار وحدة. تجنب اختبار:

  • كود الطرف الثالث: لا تختبر المكتبات أو الأطر—ثق أنها تم اختبارها بالفعل
  • الكود التافه: الحصول/التعيين البسيط لا يحتاج إلى اختبارات
  • الطرق الخاصة: اختبر الواجهات العامة؛ الطرق الخاصة هي تفاصيل التنفيذ
  • سحر الإطار: لا تختبر ميزات الإطار مثل التوجيه أو سلوك ORM

قاعدة عامة: إذا كان اختبارك يكرر فقط كود التنفيذ، فمن المحتمل أنه لا يضيف قيمة. اختبر السلوك، وليس التنفيذ.

أنماط اختبار الوحدة الشائعة

اختبار القيم المرجعة

<script> test('should return uppercase string', () => { expect(toUpperCase('hello')).toBe('HELLO'); }); </script>

اختبار الاستثناءات

<script> test('should throw error for invalid input', () => { expect(() => parseJSON('invalid')).toThrow(SyntaxError); }); </script>

اختبار تغييرات الحالة

<script> test('should update counter state', () => { const counter = new Counter(); expect(counter.value).toBe(0); counter.increment(); expect(counter.value).toBe(1); }); </script>

اختبار خصائص المصفوفة/الكائن

<script> test('should return user with correct properties', () => { const user = createUser('John', 'john@example.com'); expect(user).toHaveProperty('name', 'John'); expect(user).toHaveProperty('email', 'john@example.com'); expect(user.roles).toContain('user'); }); </script>

تمرين عملي

اكتب اختبارات وحدة لدالة التحقق هذه:

<script> function validatePassword(password) { if (!password) { return { valid: false, error: 'Password is required' }; } if (password.length < 8) { return { valid: false, error: 'Password must be at least 8 characters' }; } if (!/[A-Z]/.test(password)) { return { valid: false, error: 'Password must contain uppercase letter' }; } if (!/[0-9]/.test(password)) { return { valid: false, error: 'Password must contain a number' }; } return { valid: true }; } </script>

اكتب اختبارات لـ:

  1. كلمة مرور فارغة
  2. كلمة مرور قصيرة جدًا
  3. كلمة مرور بدون حرف كبير
  4. كلمة مرور بدون رقم
  5. كلمة مرور صالحة

الخلاصة

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

النقاط الرئيسية:

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