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

التطوير بقيادة الاختبارات (TDD)

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

التطوير بقيادة الاختبارات (TDD)

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

دورة أحمر-أخضر-إعادة الهيكلة بالتفصيل

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

مثال عملي متكامل: حسابات المال

سنبني بمنهجية TDD فئة Money بسيطة تجمع مبالغ بنفس العملة. نبدأ بلا شيء سوى ملف الاختبار.

التكرار 1 — أحمر: كائن Money يحفظ مبلغه

// MoneyTest.java import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class MoneyTest { @Test void amountIsRetained() { Money m = new Money(10, "USD"); assertEquals(10, m.amount()); } }

تشغيل الاختبار يفشل بخطأ تجميع: Money غير موجودة. هذه هي الخطوة الحمراء.

التكرار 1 — أخضر: الحد الأدنى من الكود

// Money.java record Money(int amount, String currency) {}

الاختبار الآن أخضر. قاومنا إضافة add() أو أي شيء آخر لا يتطلبه الاختبار.

التكرار 2 — أحمر: جمع أموال بنفس العملة

@Test void addSameCurrency() { Money a = new Money(10, "USD"); Money b = new Money(5, "USD"); Money result = a.add(b); assertEquals(15, result.amount()); assertEquals("USD", result.currency()); }

أحمر: add() غير موجودة.

التكرار 2 — أخضر

// Money.java record Money(int amount, String currency) { Money add(Money other) { return new Money(this.amount + other.amount, this.currency); } }

أخضر. أما مرحلة إعادة الهيكلة: هل اسم amount واضح؟ كمُستخرِج في record فنعم. لا تكرار. نكمل.

التكرار 3 — أحمر: عملات مختلفة يجب أن ترمي استثناءً

@Test void addDifferentCurrenciesThrows() { Money usd = new Money(10, "USD"); Money eur = new Money(5, "EUR"); assertThrows(IllegalArgumentException.class, () -> usd.add(eur)); }

التكرار 3 — أخضر

record Money(int amount, String currency) { Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException( "Cannot add " + other.currency + " to " + this.currency); } return new Money(this.amount + other.amount, this.currency); } }

التكرار 3 — إعادة الهيكلة

الاختبارات الثلاثة ناجحة. يمكننا استخراج الحارس في دالة خاصة لتوضيح القصد:

record Money(int amount, String currency) { Money add(Money other) { requireSameCurrency(other); return new Money(this.amount + other.amount, this.currency); } private void requireSameCurrency(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException( "Cannot add " + other.currency + " to " + this.currency); } } }

جميع الاختبارات لا تزال خضراء. إعادة الهيكلة كانت آمنة.

خذ أصغر خطوة تغيّر لون الاختبار. المحترفون في TDD يقلّصون تكراراتهم حتى تستغرق كل دورة دقيقة أو أقل. الخطوات الصغيرة تعني جلسات تصحيح صغيرة عند حدوث مشاكل.

تقنية التثليث

حين تكون الخطوة الخضراء "غشًا" (مثل return 15;)، تُثلّث بكتابة اختبار ثانٍ يُجبرك على تنفيذ حقيقي. مثلًا، اختبار لـ add(3, 5) يُعيد 8 وآخر لـ add(7, 2) يُعيد 9 يُجبرك على كتابة return a + b بدلًا من تثبيت 8.

TDD وضغط التصميم

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

  • صعب الاستنساخ — الفئة تحمل مسؤوليات كثيرة. قسّمها.
  • وجوب محاكاة قاعدة البيانات في اختبار وحدة — منطق المجال مقترن بالمثابرة. أضف واجهة repository وحقنها.
  • اختبار دالة خاصة — الدوال الخاصة تفاصيل تنفيذية. اختبر العقد العام؛ إن كان المنطق الخاص معقدًا كفاية ليحتاج اختباره الخاص، استخرجه في فئة متعاونة.
  • إعداد الاختبار يمتد 30 سطرًا — الموضوع له تبعيات كثيرة جدًا. طبّق مبدأ المسؤولية الواحدة.
TDD لا يعني أن الهدف تغطية 100%. يعني أن كل سلوك اخترت تنفيذه له اختبار قاده. البنية التحتية (دالة main، ربط DI، التسجيل) نادرًا ما تستحق TDD. ممارسة الحكم على ما يستحق الاختبار مهارة مهنية.

تطبيق TDD على طبقة الخدمات

في العالم الحقيقي، TDD غالبًا يتضمن تبعيات محقونة. المفتاح هو كتابة الاختبار أولًا مع محاكاة، مما يُجبرك على تعريف واجهة المتعاون قبل تنفيذه.

// OrderServiceTest.java import org.junit.jupiter.api.*; import org.mockito.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class OrderServiceTest { @Mock OrderRepository repo; @Mock PaymentGateway gateway; @InjectMocks OrderService service; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } // أحمر: اكتب هذا الاختبار أولًا — OrderService غير موجودة بعد @Test void placeOrder_chargesCustomerAndPersistsOrder() { Order order = new Order("customer-1", 200); when(gateway.charge("customer-1", 200)).thenReturn(true); service.placeOrder(order); verify(gateway).charge("customer-1", 200); verify(repo).save(order); } }

هذا الاختبار يُعرّف الواجهة البرمجية العامة لـ OrderService — الدالة placeOrder، وواجهات المتعاونين (OrderRepository، PaymentGateway)، والتفاعل المتوقع — كل ذلك قبل كتابة سطر واحد من OrderService.

الخلاصة

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