الاختبار باستخدام JUnit 5 وMockito

لماذا نختبر؟ أسس الاختبار البرمجي

15 دقيقة الدرس 1 من 13

لماذا نختبر؟ أسس الاختبار البرمجي

لقد بنيت أنظمة معقدة بـ Java — تطبيقات متعددة الطبقات تستخدم Generics وStreams والتزامن والوصول إلى قواعد البيانات. بهذا الحجم، قد يُسبّب null واحد في المكان الخطأ، أو افتراض خاطئ حول سلامة الخيوط، أو قراءة خاطئة لنتيجة SQL ساعاتٍ من تتبع الأخطاء في الإنتاج. الاختبارات الآلية ليست عبئًا اختياريًا؛ إنها الانضباط الهندسي الذي يجعل قواعد الشفرة الكبيرة المتطورة قابلة للإدارة.

التكلفة الحقيقية لغياب الاختبارات

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

تُزيح الاختبارات الآلية نقطة الاكتشاف تلك إلى اليسار — إلى وقت التطوير — حيث يكون الإصلاح رخيصًا.

الاختبارات توثّق النوايا أيضًا. اختبار ذو اسم واضح مثل shouldThrowWhenAmountExceedsBalance() يوصل قاعدة عمل تجارية بدقة أكبر من أي تعليق. المُطوّرون المستقبليون يقرؤون الاختبارات لفهم ما يُفترض أن يفعله الكود.

هرم الاختبار

ليست جميع الاختبارات متساوية. استقرت الصناعة على نموذج ثلاثي الطبقات — هرم الاختبار — يوازن بين التغطية والسرعة والثقة.

  • اختبارات الوحدة (القاعدة، أوسع طبقة) — تختبر فئة واحدة أو دالة نقية واحدة في عزل تام، مُستبدلةً كل التبعيات ببدائل مزيّفة. تعمل في ميلي ثوانٍ، وتعطي رسائل فشل دقيقة، وتُشكّل الجزء الأكبر من مجموعة اختبارات صحية.
  • اختبارات التكامل (الطبقة الوسطى) — تربط مكوّنَين حقيقيَّين أو أكثر معًا — خدمة مع قاعدة بيانات حقيقية، أو متحكّم مع طبقة HTTP حقيقية. تعمل بشكل أبطأ (ثوانٍ) لكنها تتحقق من أن الأجزاء تتلاءم معًا فعلًا.
  • اختبارات الطرف إلى الطرف (القمة، أضيق طبقة) — تقود التطبيق المُنشر بالكامل من منظور المستخدم: نقر زر في واجهة المستخدم، والتحقق من إنشاء سجل قاعدة البيانات. تكتشف مشكلات التوصيل عبر المكدس بأكمله لكنها بطيئة (دقائق) وهشّة ومكلفة الصيانة. أبقِها قليلة ومُركّزة على المسارات الحرجة.
اقلب الهرم على مسؤوليتك الخاصة. كثير من الفرق تبدأ باختبارات E2E فقط لأنها "تختبر كل شيء". تلك المجموعات تصبح بطيئة وهشة وصعبة الصيانة. استثمر بكثافة على مستوى الوحدة، وباعتدال في التكامل، وبقدر ضئيل في E2E.

اختبارات الوحدة في Java: شكل الاختبار

اختبار وحدة لفئة BankAccount يبدو كالتالي:

// BankAccount.java public class BankAccount { private double balance; public BankAccount(double initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException("Initial balance cannot be negative"); } this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive"); balance += amount; } public void withdraw(double amount) { if (amount <= 0) throw new IllegalArgumentException("Amount must be positive"); if (amount > balance) throw new IllegalStateException("Insufficient funds"); balance -= amount; } public double getBalance() { return balance; } }
// BankAccountTest.java (JUnit 5) import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class BankAccountTest { @Test void depositIncreasesBalance() { BankAccount account = new BankAccount(100.0); account.deposit(50.0); assertEquals(150.0, account.getBalance()); } @Test void withdrawReducesBalance() { BankAccount account = new BankAccount(200.0); account.withdraw(80.0); assertEquals(120.0, account.getBalance()); } @Test void withdrawThrowsWhenAmountExceedsBalance() { BankAccount account = new BankAccount(50.0); assertThrows(IllegalStateException.class, () -> account.withdraw(100.0)); } }

يتّبع كل اختبار نمط ترتيب–تصرّف–تحقّق (Arrange–Act–Assert)، المعروف أيضًا بـ Given–When–Then في التطوير القائم على السلوك (BDD). الترتيب: إعداد النظام قيد الاختبار ومدخلاته. التصرّف: استدعاء التابع المُختبَر. التحقّق: التحقق من النتيجة.

اختبار التكامل: خدمة مع مستودع حقيقي

عند طبقة التكامل تتوقف عن تزوير المتعاونين وتترك الحقيقيين يشاركون:

// AccountServiceIntegrationTest.java // يستخدم قاعدة بيانات H2 في الذاكرة مُوصَّلة عبر Spring Data JPA @SpringBootTest @Transactional class AccountServiceIntegrationTest { @Autowired private AccountService accountService; @Autowired private AccountRepository repository; @Test void transferPersistsBothDebitAndCredit() { Account sender = repository.save(new Account("Alice", 500.0)); Account receiver = repository.save(new Account("Bob", 100.0)); accountService.transfer(sender.getId(), receiver.getId(), 200.0); Account updatedSender = repository.findById(sender.getId()).orElseThrow(); Account updatedReceiver = repository.findById(receiver.getId()).orElseThrow(); assertEquals(300.0, updatedSender.getBalance()); assertEquals(300.0, updatedReceiver.getBalance()); } }

يكتشف هذا الاختبار أخطاء لا يستطيع اختبار الوحدة اكتشافها — على سبيل المثال، خطأ في تهيئة حدود المعاملة (transaction boundary) يُلغي نصف التحويل فقط.

الخصائص الأساسية للاختبارات الجيدة

تُعرّف مبادئ F.I.R.S.T. الشكل المطلوب لمجموعة الاختبارات:

  • سريعة (Fast) — يجب أن تعمل اختبارات الوحدة في ميلي ثوانٍ؛ والمجموعة الكاملة في ثوانٍ. الاختبارات البطيئة يتم تجاهلها.
  • معزولة (Isolated) — كل اختبار مستقل. لا توجد حالة قابلة للتغيير مشتركة بين الاختبارات. يمكن تشغيل الاختبارات بأي ترتيب.
  • قابلة للتكرار (Repeatable) — ينتج الاختبار ذاته النتيجة ذاتها دائمًا بصرف النظر عن البيئة أو الوقت أو حالة الشبكة.
  • ذاتية التحقق (Self-validating) — يُبلّغ الاختبار بنفسه بالنجاح أو الفشل. لا إنسان يقرأ ملف سجل ليقرر.
  • في الوقت المناسب (Timely) — تُكتب الاختبارات في نفس وقت كتابة كود الإنتاج (أو قبله)، لا بعد ستة أشهر.
اختبار يجتاز دائمًا أسوأ من لا اختبار على الإطلاق. إذا كتبت تأكيدات لا يمكن أن تفشل أبدًا — مثل assertTrue(true)، أو تأكيد على قيمة لا تُغيّرها فعليًا — ستحصل على ثقة زائفة. كل تأكيد يجب أن يكون قادرًا على اصطياد خطأ حقيقي.

تغطية الكود: أداة لا هدف

تقيس تغطية الكود نسبة كود الإنتاج الذي ينفّذه اختبار واحد على الأقل. تغطية 80% للأسطر هدف معقول لكثير من المشاريع، لكن الرقم ليس الغاية. مجموعة اختبارات بتغطية 95% بلا تأكيدات ذات معنى عديمة الفائدة. أما مجموعة بتغطية 70% تُؤكّد كل قاعدة عمل مهمة وكل حالة حافة فهي ممتازة.

استخدم التغطية لاكتشاف الثغرات — الفروع غير المُختبَرة ومسارات الخطأ والحالات الحافة — لا للوصول إلى رقم معين والإعلان عن النصر.

الخلاصة

الاختبارات الآلية قرار اقتصادي بقدر ما هي تقني. تُزيح اكتشاف العيوب إلى اليسار، وتوثّق النوايا، وتُمكّن من إعادة الهيكلة بأمان. يُرشد هرم الاختبار كيفية توزيع الجهد: اختبارات وحدة كثيرة وسريعة، واختبارات تكامل أقل، واختبارات E2E قليلة جدًا. كل اختبار يتبع نمط Arrange–Act–Assert، وكل تأكيد يجب أن يكون قادرًا على الفشل. بهذه الأسس في مكانها، أنت مستعد لتعلم الأدوات المحددة — JUnit 5 وMockito — في الدروس التالية.