دليل لاختبارات الوحدة القوية والتكامل مع JUnit
نشرت: 2022-03-11تعد اختبارات البرامج المؤتمتة مهمة للغاية للجودة طويلة المدى ، وقابلية الصيانة ، وقابلية التوسع لمشاريع البرامج ، وبالنسبة لجافا ، فإن JUnit هي الطريق إلى الأتمتة.
في حين أن معظم هذه المقالة ستركز على كتابة اختبارات وحدة قوية واستخدام stubbing ، والسخرية ، وحقن التبعية ، سنناقش أيضًا اختبارات JUnit والتكامل.
إطار عمل اختبار JUnit هو أداة شائعة ومجانية ومفتوحة المصدر لاختبار المشاريع القائمة على Java.
حتى كتابة هذه السطور ، JUnit 4 هو الإصدار الرئيسي الحالي ، حيث تم إصداره منذ أكثر من 10 سنوات ، وكان آخر تحديث منذ أكثر من عامين.
JUnit 5 (مع نماذج برمجة وامتداد المشتري) قيد التطوير النشط. يدعم بشكل أفضل ميزات اللغة المقدمة في Java 8 ويتضمن ميزات أخرى جديدة ومثيرة للاهتمام. قد تجد بعض الفرق أن JUnit 5 جاهزة للاستخدام ، بينما قد يستمر البعض الآخر في استخدام JUnit 4 حتى يتم إصدار 5 رسميًا. سننظر في أمثلة من كليهما.
تشغيل JUnit
يمكن تشغيل اختبارات JUnit مباشرةً في IntelliJ ، ولكن يمكن أيضًا تشغيلها في IDEs الأخرى مثل Eclipse أو NetBeans أو حتى سطر الأوامر.
يجب إجراء الاختبارات دائمًا في وقت الإنشاء ، وخاصة اختبارات الوحدة. يجب اعتبار الإنشاء مع أي اختبارات فاشلة فاشلاً ، بغض النظر عما إذا كانت المشكلة في الإنتاج أو رمز الاختبار - يتطلب هذا الانضباط من الفريق والاستعداد لإعطاء أولوية قصوى لحل الاختبارات الفاشلة ، ولكن من الضروري الالتزام بـ روح الأتمتة.
يمكن أيضًا تشغيل اختبارات JUnit والإبلاغ عنها بواسطة أنظمة التكامل المستمر مثل Jenkins. تتمتع المشاريع التي تستخدم أدوات مثل Gradle أو Maven أو Ant بميزة إضافية تتمثل في القدرة على إجراء الاختبارات كجزء من عملية الإنشاء.
جرادل
كنموذج لمشروع Gradle لـ JUnit 5 ، راجع قسم Gradle من دليل مستخدم JUnit ومستودع junit5-sample.git. لاحظ أنه يمكن أيضًا تشغيل الاختبارات التي تستخدم واجهة برمجة تطبيقات JUnit 4 (يشار إليها باسم "عتيقة" ).
يمكن إنشاء المشروع في IntelliJ عبر خيار القائمة File> Open…> انتقل إلى junit-gradle-consumer sub-directory
> OK> Open as Project> OK لاستيراد المشروع من Gradle.
بالنسبة إلى Eclipse ، يمكن تثبيت المكون الإضافي Buildship Gradle من Help> Eclipse Marketplace… يمكن بعد ذلك استيراد المشروع باستخدام ملف> استيراد…> Gradle> Gradle Project> التالي> التالي> استعرض للوصول إلى الدليل الفرعي junit-gradle-consumer
> التالي > التالي> إنهاء.
بعد إعداد مشروع Gradle في IntelliJ أو Eclipse ، سيتضمن تشغيل مهمة build
Gradle تشغيل جميع اختبارات JUnit مع مهمة test
. لاحظ أنه قد يتم تخطي الاختبارات في عمليات التنفيذ اللاحقة build
إذا لم يتم إجراء تغييرات على الكود.
بالنسبة إلى JUnit 4 ، راجع استخدام JUnit مع Gradle wiki.
مخضرم
بالنسبة إلى JUnit 5 ، ارجع إلى قسم Maven من دليل المستخدم ومستودع junit5-sample.git للحصول على مثال لمشروع Maven. يمكن لهذا أيضًا إجراء اختبارات قديمة (تلك التي تستخدم JUnit 4 API).
في IntelliJ ، استخدم ملف> فتح…> انتقل إلى junit-maven-consumer/pom.xml
> موافق> فتح كمشروع. يمكن بعد ذلك تشغيل الاختبارات من Maven Projects> junit5-maven-Consumer> Lifecycle> Test.
في Eclipse ، استخدم File> Import…> Maven> Existing Maven Projects> Next> تصفح إلى دليل junit-maven-consumer
> مع تحديد pom.xml
> إنهاء.
يمكن تنفيذ الاختبارات عن طريق تشغيل المشروع مثل Maven build…> تحديد هدف test
> تشغيل.
بالنسبة إلى JUnit 4 ، راجع JUnit في مستودع Maven.
بيئات التنمية
بالإضافة إلى إجراء الاختبارات من خلال أدوات الإنشاء مثل Gradle أو Maven ، يمكن للعديد من IDEs تشغيل اختبارات JUnit مباشرةً.
IntelliJ IDEA
مطلوب IntelliJ IDEA 2016.2 أو أحدث لاختبارات JUnit 5 ، بينما يجب أن تعمل اختبارات JUnit 4 في إصدارات IntelliJ الأقدم.
لأغراض هذه المقالة ، قد ترغب في إنشاء مشروع جديد في IntelliJ من أحد مستودعات GitHub الخاصة بي (JUnit5IntelliJ.git أو JUnit4IntelliJ.git) ، والتي تتضمن جميع الملفات في مثال فئة Person
البسيط واستخدام العنصر المدمج مكتبات JUnit. يمكن إجراء الاختبار باستخدام Run> Run "All Tests". يمكن أيضًا إجراء الاختبار في IntelliJ من فئة PersonTest
.
تم إنشاء هذه المستودعات باستخدام مشاريع IntelliJ Java الجديدة وبناء هياكل الدليل src/main/java/com/example
و src/test/java/com/example
. تم تحديد دليل src/main/java
كمجلد مصدر بينما تم تحديد src/test/java
كمجلد مصدر اختبار. بعد إنشاء فئة PersonTest
مع طريقة اختبار مشروحة @Test
، فقد تفشل في التجميع ، وفي هذه الحالة تقدم IntelliJ اقتراحًا لإضافة JUnit 4 أو JUnit 5 إلى مسار الفصل الذي يمكن تحميله من توزيع IntelliJ IDEA (انظر هذه إجابات على Stack Overflow لمزيد من التفاصيل). أخيرًا ، تمت إضافة تكوين تشغيل JUnit لجميع الاختبارات.
راجع أيضًا إرشادات إرشادات اختبار IntelliJ.
كسوف
لن يحتوي مشروع Java فارغ في Eclipse على دليل جذر اختبار. تمت إضافة هذا من خصائص المشروع> Java Build Path> Add Folder…> Create New Folder…> حدد اسم المجلد> إنهاء. سيتم تحديد الدليل الجديد كمجلد مصدر. انقر فوق "موافق" في كلا مربعي الحوار المتبقيين.
يمكن إنشاء اختبارات JUnit 4 باستخدام File> New> JUnit Test Case. حدد "New JUnit 4 test" ومجلد المصدر الذي تم إنشاؤه حديثًا للاختبارات. حدد "فئة قيد الاختبار" و "حزمة" ، مع التأكد من تطابق الحزمة مع الفئة قيد الاختبار. ثم حدد اسمًا لفئة الاختبار. بعد الانتهاء من المعالج ، إذا طُلب منك ذلك ، اختر "إضافة مكتبة JUnit 4" إلى مسار البناء. يمكن بعد ذلك تشغيل فئة المشروع أو الاختبار الفردي كاختبار JUnit. راجع أيضًا كتابة Eclipse Writing وتشغيل اختبارات JUnit.
NetBeans
يدعم NetBeans اختبارات JUnit 4 فقط. يمكن إنشاء فئات الاختبار في مشروع NetBeans Java باستخدام ملف> ملف جديد…> اختبارات الوحدة> اختبار JUnit أو اختبار للفصل الحالي. بشكل افتراضي ، يسمى الدليل الجذر test
في دليل المشروع.
فئة الإنتاج البسيطة وحالة اختبار JUnit الخاصة بها
دعنا نلقي نظرة على مثال بسيط لكود الإنتاج وكود اختبار الوحدة المقابل لفئة Person
بسيطة للغاية. يمكنك تنزيل نموذج الكود من مشروع github الخاص بي وفتحه عبر IntelliJ.
src / main / java / com / example / Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
تحتوي فئة Person
غير القابلة للتغيير على مُنشئ وطريقة getDisplayName()
. نريد اختبار أن getDisplayName()
ترجع الاسم المنسق كما نتوقع. إليك كود الاختبار لاختبار وحدة واحدة (الوحدة 5):
src / test / java / com / example / PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
يستخدم PersonTest
لـ JUnit 5's @Test
. بالنسبة للوحدة JUnit 4 ، يجب أن تكون فئة وطريقة PersonTest
ويجب استخدام عمليات استيراد مختلفة. إليك مثال JUnit 4 Gist.
عند تشغيل فئة PersonTest
في IntelliJ ، يمر الاختبار وتظهر مؤشرات واجهة المستخدم باللون الأخضر.
اتفاقيات الوحدة المشتركة
تسمية
على الرغم من أنه ليس مطلوبًا ، فإننا نستخدم الاصطلاحات الشائعة في تسمية فئة الاختبار ؛ على وجه التحديد ، نبدأ باسم الفصل الذي يتم اختباره ( Person
) ونلحق به "اختبار" ( PersonTest
). تتشابه تسمية طريقة الاختبار ، بدءًا من الطريقة التي يتم اختبارها ( getDisplayName()
) والإعداد المسبق لـ "test" ( testGetDisplayName()
). في حين أن هناك العديد من الاصطلاحات الأخرى المقبولة تمامًا لتسمية طرق الاختبار ، فمن المهم أن تكون متسقًا عبر الفريق والمشروع.
الاسم في الإنتاج | الاسم في الاختبار |
---|---|
شخص | اختبار الشخص |
getDisplayName() | testDisplayName() |
الحزم
نحن نستخدم أيضًا اصطلاحًا لإنشاء فئة PersonTest
لكود الاختبار في نفس الحزمة ( com.example
) مثل فئة Person
في كود الإنتاج. إذا استخدمنا حزمة مختلفة للاختبارات ، فسنطلب منا استخدام مُعدِّل الوصول العام في فئات كود الإنتاج ، والمنشئات ، والطرق المشار إليها بواسطة اختبارات الوحدة ، حتى عندما لا يكون ذلك مناسبًا ، لذلك من الأفضل الاحتفاظ بها في نفس الحزمة . ومع ذلك ، فإننا نستخدم أدلة مصادر منفصلة ( src/main/java
and src/test/java
) لأننا عمومًا لا نريد تضمين كود الاختبار في إصدارات الإنتاج الصادرة.
الهيكل والشروح
@Test
التعليق التوضيحيTest (JUnit 4/5) JUnit بتنفيذ طريقة testGetDisplayName()
كطريقة اختبار والإبلاغ عما إذا كان يمر أو يفشل. طالما أن جميع التأكيدات (إن وجدت) تنجح ولم يتم طرح أي استثناءات ، يعتبر الاختبار ناجحًا.
يتبع كود الاختبار الخاص بنا نمط هيكل Arrange-Act-Assert (AAA). تشتمل الأنماط الشائعة الأخرى على Given-when-Then و Setup-Exercise-Verify-Teardown (عادةً لا يكون التمزيق مطلوبًا بشكل صريح لاختبارات الوحدة) ، لكننا نستخدم AAA في هذه المقالة.
دعنا نلقي نظرة على كيفية اتباع مثال الاختبار لدينا AAA. السطر الأول ، "الترتيب" ينشئ كائن Person
الذي سيتم اختباره:
Person person = new Person("Josh", "Hayden");
السطر الثاني ، "الفعل" ، يمارس طريقة Person.getDisplayName()
في كود الإنتاج:
String displayName = person.getDisplayName();
السطر الثالث ، "التأكيد" ، يتحقق من أن النتيجة كما هو متوقع.
assertEquals("Hayden, Josh", displayName);
داخليًا ، يستخدم استدعاء assertEquals()
طريقة يساوي "Hayden، Josh" String لكائن String للتحقق من القيمة الفعلية التي تم إرجاعها من مطابقة كود الإنتاج (اسم displayName
). إذا لم يتطابق ، فسيتم وضع علامة على الاختبار بأنه فشل.
لاحظ أن الاختبارات غالبًا ما تحتوي على أكثر من سطر واحد لكل مرحلة من مراحل AAA هذه.
اختبارات الوحدة وكود الإنتاج
الآن بعد أن غطينا بعض اتفاقيات الاختبار ، دعنا نوجه انتباهنا إلى جعل كود الإنتاج قابلاً للاختبار.
نعود إلى فئة Person
، حيث قمت بتطبيق طريقة لإعادة عمر الشخص بناءً على تاريخ ميلاده. تتطلب أمثلة التعليمات البرمجية Java 8 للاستفادة من التاريخ الجديد وواجهات برمجة التطبيقات الوظيفية. هذا ما تبدو عليه فئة Person.java
الجديدة:
شخص. جافا
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
يعلن تشغيل هذا الفصل (في وقت كتابة هذا التقرير) أن جوي يبلغ من العمر 4 سنوات. دعنا نضيف طريقة اختبار:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
إنه يمر اليوم ، ولكن ماذا عن عندما نقوم بتشغيله بعد عام من الآن؟ هذا الاختبار غير حتمي وهش لأن النتيجة المتوقعة تعتمد على التاريخ الحالي للنظام الذي يجري الاختبار.
إيقاف وحقن مورد ذي قيمة
عند التشغيل في الإنتاج ، نريد استخدام التاريخ الحالي ، LocalDate.now()
، لحساب عمر الشخص ، ولكن لإجراء اختبار حتمي حتى بعد عام من الآن ، تحتاج الاختبارات إلى توفير قيم currentDate
الخاصة بها.
يُعرف هذا بحقن التبعية. لا نريد أن يحدد كائن Person
لدينا التاريخ الحالي نفسه ، ولكن بدلاً من ذلك نريد تمرير هذا المنطق على أنه تبعية. ستستخدم اختبارات الوحدة قيمة معروفة ومرتقدة ، وسيسمح رمز الإنتاج بتوفير القيمة الفعلية من قبل النظام في وقت التشغيل.
دعنا نضيف مورد LocalDate
إلى Person.java
:
شخص. جافا
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
لتسهيل اختبار طريقة getAge()
، قمنا بتغييرها لاستخدام currentDateSupplier
، مورد LocalDate
، لاسترداد التاريخ الحالي. إذا كنت لا تعرف ما هو المورد ، فإنني أوصي بالقراءة عن واجهات Lambda الوظيفية المدمجة.
أضفنا أيضًا إدخال التبعية: تسمح مُنشئ الاختبار الجديد للاختبارات بتوفير قيم التاريخ الحالية الخاصة بها. يستدعي المُنشئ الأصلي هذا المُنشئ الجديد ، ويمرر مرجع أسلوب ثابت لـ LocalDate::now
، والذي يوفر كائن LocalDate
، لذلك لا تزال طريقتنا الرئيسية تعمل كما كانت من قبل. ماذا عن طريقة الاختبار لدينا؟ لنقم بتحديث PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
يضخ الاختبار الآن قيمة currentDate
الخاصة به ، لذلك سيستمر اختبارنا في اجتيازه عند إجرائه العام المقبل ، أو خلال أي عام. يشار إلى هذا عادةً باسم stubbing ، أو تقديم قيمة معروفة ليتم إرجاعها ، ولكن كان علينا أولاً تغيير Person
للسماح بحقن هذه التبعية.
لاحظ صيغة lambda ( ()->currentDate
) عند إنشاء كائن Person
. يتم التعامل مع هذا كمورد LocalDate
، كما هو مطلوب من قبل المُنشئ الجديد.
السخرية من خدمة الويب
نحن جاهزون لكائن Person
- الذي كان وجوده بالكامل في ذاكرة JVM - للتواصل مع العالم الخارجي. نريد إضافة طريقتين: طريقة publishAge()
، والتي ستنشر العمر الحالي للشخص ، وطريقة getThoseInCommon()
، والتي ستعيد أسماء المشاهير الذين يتشاركون في نفس تاريخ الميلاد أو هم نفس عمر Person
. لنفترض أن هناك خدمة RESTful يمكننا التفاعل معها تسمى "أعياد ميلاد الأشخاص". لدينا عميل Java له يتكون من فئة واحدة ، BirthdaysClient
.
com.example.birthdays. أعياد الميلاد العميل
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
دعونا نحسن فئة Person
لدينا. نبدأ بإضافة طريقة اختبار جديدة للسلوك المرغوب لـ publishAge()
. لماذا تبدأ بالاختبار بدلاً من الوظيفة؟ نحن نتبع مبادئ التطوير القائم على الاختبار (المعروف أيضًا باسم TDD) ، حيث نكتب الاختبار أولاً ، ثم الكود الذي نجتازه.
PersonTest.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
في هذه المرحلة ، فشل رمز الاختبار في التحويل لأننا لم ننشئ طريقة publishAge()
التي تستدعيها. بمجرد إنشاء طريقة Person.publishAge()
فارغة ، يمر كل شيء. نحن الآن جاهزون للاختبار للتحقق من نشر عمر الشخص بالفعل إلى BirthdaysClient
.

إضافة كائن تم الاستهزاء به
نظرًا لأن هذا اختبار وحدة ، يجب أن يتم تشغيله بسرعة وفي الذاكرة ، لذلك سيقوم الاختبار ببناء كائن Person
الخاص بنا باستخدام عميل BirthdaysClient
وهمي بحيث لا يقوم في الواقع بتقديم طلب ويب. سيستخدم الاختبار بعد ذلك هذا الكائن الوهمي للتحقق من أنه تم استدعاؤه كما هو متوقع. للقيام بذلك ، سنضيف تبعية إلى إطار عمل Mockito (ترخيص MIT) لإنشاء كائنات وهمية ، ثم إنشاء كائن BirthdaysClient
سخر منه:
PersonTest.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
علاوة على ذلك ، قمنا بزيادة توقيع منشئ Person
لأخذ كائن BirthdaysClient
، وقمنا بتغيير الاختبار لحقن كائن BirthdaysClient
المزعج.
إضافة توقع وهمي
بعد ذلك ، نضيف إلى نهاية testPublishAge
توقعًا بأن يتم استدعاء عميل BirthdaysClient
. يجب Person.publishAge()
، كما هو موضح في PersonTest.java
الجديد:
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
يتتبع عميل BirthdaysClient
المُحسَّن من قبل Mockito جميع المكالمات التي تم إجراؤها لطرقه ، وهذه هي الطريقة التي نتحقق بها من عدم إجراء مكالمات إلى BirthdaysClient
باستخدام طريقة التحقق من verifyZeroInteractions()
قبل الاتصال بـ publishAge()
. على الرغم من أنه ليس ضروريًا ، إلا أننا من خلال القيام بذلك نضمن أن المُنشئ لا يقوم بأي استدعاءات مخادعة. في سطر verify()
، نحدد كيف نتوقع أن تبدو المكالمة إلى BirthdaysClient
.
لاحظ أنه نظرًا لأن publishRegularPersonAge يحتوي على IOException في توقيعه ، فإننا نضيفه أيضًا إلى توقيع أسلوب الاختبار الخاص بنا.
في هذه المرحلة ، يفشل الاختبار:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
هذا متوقع ، نظرًا لأننا لم ننفذ التغييرات المطلوبة على Person.java
، نظرًا لأننا نتابع التطوير القائم على الاختبار. سنقوم الآن باجتياز هذا الاختبار من خلال إجراء التغييرات اللازمة:
شخص. جافا
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
اختبار الاستثناءات
لقد جعلنا مُنشئ كود الإنتاج ينشئ نموذجًا جديدًا لـ BirthdaysClient
، ويستدعي publishAge()
الآن birthdaysClient
. اجتازت جميع الاختبارات ؛ كل شيء أخضر. رائعة! لكن لاحظ أن publishAge()
يبتلع IOException. بدلاً من تركها تنفجر ، نريد أن نلفها مع PersonException الخاص بنا في ملف جديد يسمى PersonException.java
:
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
نقوم بتنفيذ هذا السيناريو كطريقة اختبار جديدة في PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
يقوم استدعاء birthdaysClient
doThrow()
باستدعاء BirthdaysClient لطرح استثناء عند استدعاء طريقة publishRegularPersonAge()
. إذا لم يتم طرح PersonException
، فإننا نفشل في الاختبار. وإلا فإننا نؤكد أن الاستثناء تم ربطه بشكل صحيح مع IOException والتحقق من أن رسالة الاستثناء كما هو متوقع. في الوقت الحالي ، نظرًا لأننا لم ننفذ أي معالجة في كود الإنتاج الخاص بنا ، فقد فشل اختبارنا لأنه لم يتم طرح الاستثناء المتوقع. إليك ما نحتاج إلى تغييره في Person.java
لاجتياز الاختبار:
شخص. جافا
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
بذرة: الأنين والتأكيدات
نقوم الآن بتطبيق طريقة Person.getThoseInCommon()
، مما يجعل فئة Person.Java
تبدو بهذا الشكل.
testGetThoseInCommon()
، على عكس testPublishAge()
، من إجراء مكالمات معينة لأساليب عملاء birthdaysClient
. بدلاً من ذلك ، يستخدم when
استدعاءات كعب روتين إرجاع القيم للمكالمات للبحث عن findFamousNamesOfAge()
و findFamousNamesBornOn()
التي سيتعين على getThoseInCommon () getThoseInCommon()
. ثم نؤكد أن جميع الأسماء الثلاثة التي قدمناها قد تم إرجاعها.
يتيح التفاف التأكيدات المتعددة باستخدام طريقة assertAll()
JUnit 5 فحص جميع التأكيدات ككل ، بدلاً من التوقف بعد أول تأكيد فاشل. نقوم أيضًا بتضمين رسالة مع assertTrue()
لتحديد أسماء معينة غير مدرجة. إليك ما تبدو عليه طريقة اختبار "المسار السعيد" (السيناريو المثالي) (ملاحظة ، هذه ليست مجموعة قوية من الاختبارات بطبيعتها لكونك "طريقًا سعيدًا" ، لكننا سنتحدث عن السبب لاحقًا.
PersonTest.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
حافظ على نظافة كود الاختبار
على الرغم من التغاضي عنه في كثير من الأحيان ، إلا أنه من المهم بنفس القدر الحفاظ على كود الاختبار خاليًا من التكرار المتفاقم. تعتبر التعليمات البرمجية والمبادئ النظيفة مثل "لا تكرر نفسك" مهمة جدًا للحفاظ على قاعدة بيانات عالية الجودة ، وكود الإنتاج والاختبار على حدٍ سواء. لاحظ أن أحدث إصدار من PersonTest.java يحتوي على بعض التكرار الآن بعد أن أصبح لدينا العديد من طرق الاختبار.
لإصلاح ذلك ، يمكننا القيام ببعض الأشياء:
قم باستخراج كائن IOException إلى حقل نهائي خاص.
استخرج إنشاء كائن
Person
إلى طريقته الخاصة (createJoeSixteenJan2()
، في هذه الحالة) حيث يتم إنشاء معظم كائنات الشخص بنفس المعلمات.قم بإنشاء
assertCauseAndMessage()
للاختبارات المختلفة التي تتحققPersonExceptions
التي تم إلقاؤها.
يمكن رؤية نتائج الشفرة النظيفة في هذا الإصدار لملف PersonTest.java.
اختبر أكثر من الطريق السعيد
ماذا يجب أن نفعل عندما يكون لكائن " Person
" تاريخ ميلاد بعد التاريخ الحالي؟ غالبًا ما تكون العيوب في التطبيقات ناتجة عن إدخال غير متوقع أو نقص في البصيرة في حالات الزاوية أو الحافة أو الحدود. من المهم محاولة توقع هذه المواقف بأفضل ما نستطيع ، وغالبًا ما تكون اختبارات الوحدة هي المكان المناسب للقيام بذلك. في بناء اختبار Person
PersonTest
، قمنا بتضمين بعض الاختبارات للاستثناءات المتوقعة ، لكنها لم تكن كاملة بأي حال من الأحوال. على سبيل المثال ، نستخدم LocalDate
الذي لا يمثل بيانات المنطقة الزمنية أو يخزنها. ومع ذلك ، تقوم مكالماتنا إلى LocalDate.now()
بإرجاع LocalDate
استنادًا إلى المنطقة الزمنية الافتراضية للنظام ، والتي يمكن أن تكون قبل يوم أو بعد مستخدم النظام. يجب مراعاة هذه العوامل مع الاختبارات المناسبة وتنفيذ السلوك.
يجب أيضًا اختبار الحدود. ضع في اعتبارك كائن Person
باستخدام طريقة getDaysUntilBirthday()
. يجب أن يشمل الاختبار ما إذا كان عيد ميلاد الشخص قد مر بالفعل في العام الحالي ، وما إذا كان عيد ميلاد الشخص هو اليوم ، وكيف تؤثر سنة كبيسة على عدد الأيام. يمكن تغطية هذه السيناريوهات عن طريق التحقق من يوم واحد قبل عيد ميلاد الشخص ، ويوم ، ويوم واحد بعد عيد ميلاد الشخص حيث يكون العام التالي سنة كبيسة. هذا هو رمز الاختبار المناسب:
PersonTest.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
اختبارات التكامل
لقد ركزنا في الغالب على اختبارات الوحدة ، ولكن يمكن أيضًا استخدام JUnit للتكامل والقبول والوظيفية واختبارات النظام. تتطلب مثل هذه الاختبارات غالبًا المزيد من كود الإعداد ، على سبيل المثال ، بدء تشغيل الخوادم ، وتحميل قواعد البيانات ببيانات معروفة ، وما إلى ذلك. بينما يمكننا غالبًا إجراء آلاف اختبارات الوحدة في ثوانٍ ، قد تستغرق مجموعات اختبار التكامل الكبيرة دقائق أو حتى ساعات للتشغيل. لا ينبغي عمومًا استخدام اختبارات التكامل لمحاولة تغطية كل تبديل أو مسار عبر الكود ؛ اختبارات الوحدة أكثر ملاءمة لذلك.
عادةً ما يتم إجراء اختبارات لتطبيقات الويب التي تدفع متصفحات الويب في ملء النماذج ، والنقر فوق الأزرار ، وانتظار تحميل المحتوى ، وما إلى ذلك ، باستخدام Selenium WebDriver (ترخيص Apache 2.0) إلى جانب "نمط كائن الصفحة" (راجع موقع SeleniumHQ github wiki ومقال مارتن فاولر عن Page Objects).
تعد JUnit فعالة في اختبار واجهات برمجة تطبيقات RESTful باستخدام عميل HTTP مثل Apache HTTP Client أو Spring Rest Template (يوفر HowToDoInJava.com مثالاً جيدًا).
في حالتنا مع كائن Person
، يمكن أن يتضمن اختبار التكامل استخدام عميل BirthdaysClient
الحقيقي بدلاً من عميل وهمي ، مع تكوين يحدد عنوان URL الأساسي لخدمة أعياد ميلاد الأشخاص. سيستخدم اختبار التكامل بعد ذلك مثيلًا اختباريًا لمثل هذه الخدمة ، والتحقق من نشر أعياد الميلاد إليه وإنشاء أشخاص مشهورين في الخدمة التي سيتم إرجاعها.
ميزات JUnit الأخرى
لدى JUnit العديد من الميزات الإضافية التي لم نستكشفها بعد في الأمثلة. سوف نصف بعض ونقدم مراجع للآخرين.
تركيبات الاختبار
وتجدر الإشارة إلى أن JUnit تنشئ مثيلًا جديدًا لفئة الاختبار لتشغيل كل طريقة @Test
. توفر JUnit أيضًا روابط التعليقات التوضيحية لتشغيل طرق معينة قبل أو بعد كل أو كل من طرق @Test
. غالبًا ما تُستخدم هذه الخطافات لإعداد أو تنظيف قاعدة بيانات أو كائنات وهمية ، وتختلف بين الوحدة 4 و 5.
الوحدة 4 | الوحدة 5 | لطريقة ثابتة؟ |
---|---|---|
@BeforeClass | @BeforeAll | نعم |
@AfterClass | @AfterAll | نعم |
@Before | @BeforeEach | رقم |
@After | @AfterEach | رقم |
في مثال PersonTest
بنا ، اخترنا تكوين كائن محاكاة " BirthdaysClient
" في أساليب @Test
نفسها ، ولكن في بعض الأحيان تحتاج الهياكل الوهمية الأكثر تعقيدًا إلى الإنشاء التي تتضمن كائنات متعددة. غالبًا ما @BeforeEach
(في JUnit 5) و @Before
(في الوحدة 4) مناسبًا لهذا الغرض.
التعليقات التوضيحية @After*
أكثر شيوعًا مع اختبارات التكامل من اختبارات الوحدة حيث تتعامل مجموعة JVM المهملة مع معظم الكائنات التي تم إنشاؤها لاختبارات الوحدة. تُستخدم التعليقات التوضيحية @BeforeClass
و @BeforeAll
بشكل شائع لاختبارات التكامل التي تحتاج إلى تنفيذ إجراءات إعداد وتفكيك مكلفة مرة واحدة ، بدلاً من كل طريقة اختبار.
بالنسبة للوحدة 4 ، يرجى الرجوع إلى دليل تجهيزات الاختبار (لا تزال المفاهيم العامة تنطبق على الوحدة 5).
مجموعات الاختبار
قد ترغب أحيانًا في إجراء عدة اختبارات ذات صلة ، ولكن ليس كل الاختبارات. في هذه الحالة ، يمكن تجميع مجموعات الاختبارات في مجموعات اختبار. لمعرفة كيفية القيام بذلك في JUnit 5 ، راجع مقالة HowToProgram.xyz's JUnit 5 ، وفي وثائق فريق JUnit لـ JUnit 4.
JUnit 5'sNested وDisplayName
يضيف JUnit 5 القدرة على استخدام فئات داخلية متداخلة غير ثابتة لإظهار العلاقة بين الاختبارات بشكل أفضل. يجب أن يكون هذا مألوفًا جدًا لأولئك الذين عملوا مع وصف متداخل في أطر اختبار مثل Jasmine for JavaScript. يتم وضع تعليقات توضيحية على الفئات الداخلية باستخدام @Nested
لاستخدام هذا.
يعد التعليق التوضيحي @DisplayName
جديدًا أيضًا على JUnit 5 ، مما يتيح لك وصف اختبار إعداد التقارير بتنسيق سلسلة ، ليتم عرضه بالإضافة إلى معرف طريقة الاختبار.
على الرغم من أنه يمكن استخدام @Nested
و @DisplayName
بشكل مستقل عن بعضهما البعض ، إلا أنهما يمكنهما توفير نتائج اختبار أوضح تصف سلوك النظام.
هامكريست ماتشرز
إطار Hamcrest ، على الرغم من أنه ليس جزءًا من قاعدة كود JUnit ، يوفر بديلاً لاستخدام طرق التأكيد التقليدية في الاختبارات ، مما يسمح برمز اختبار أكثر تعبيرًا وقابلية للقراءة. راجع التحقق التالي باستخدام كل من assertEquals التقليدي وتأكيد Hamcrest على أن:
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
يمكن استخدام Hamcrest مع كل من JUnit 4 و 5. برنامج Vogella.com التعليمي على Hamcrest شامل تمامًا.
مصادر إضافية
تغطي مقالة اختبارات الوحدة وكيفية كتابة التعليمات البرمجية القابلة للاختبار وسبب أهميتها أمثلة أكثر تحديدًا لكتابة تعليمات برمجية نظيفة وقابلة للاختبار.
البناء بثقة: يفحص دليل اختبارات JUnit الأساليب المختلفة لاختبار الوحدة والتكامل ، ولماذا من الأفضل اختيار واحد والالتزام به
يعد دليل مستخدم JUnit 4 Wiki و JUnit 5 دائمًا نقطة مرجعية ممتازة.
توفر وثائق Mockito معلومات حول الوظائف الإضافية والأمثلة.
JUnit هو الطريق إلى الأتمتة
لقد اكتشفنا العديد من جوانب الاختبار في عالم Java باستخدام JUnit. لقد نظرنا في اختبارات الوحدة والتكامل باستخدام إطار عمل JUnit لقواعد Java البرمجية ، ودمج JUnit في بيئات التطوير والبناء ، وكيفية استخدام mocks and stubs مع الموردين و Mockito ، والاتفاقيات المشتركة وأفضل ممارسات الكود ، وما الذي يجب اختباره ، وبعض من ميزات JUnit الرائعة الأخرى.
لقد حان دور القارئ الآن للنمو في تطبيق الاختبارات الآلية والحفاظ عليها وجني فوائدها بمهارة باستخدام إطار عمل JUnit.