تلميحات للاختبارات الوحدة مع AssertJ

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

في هذه المقالة، ستتعلم

لا تستخدم فحص NPE بشكل مفرط

نحن نميل جميعًا لتجنب NullPointerException بقدر الإمكان في الكود الرئيسي لأنه يمكن أن يؤدي إلى عواقب قبيحة. أعتقد أن محاولتنا الرئيسية ليست تجنب NPE في الاختبارات. هدفنا هو التحقق من سلوك المكون المختبر بطريقة نظيفة وقابلة للقراءة وموثوق بها.

ممارسة سيئة

في كثير من الأحيان في الماضي، استخدمت isNotNull الإعتراض حتى عندما لم يكن مطلوبًا، كما في المثال أدناه:

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

ينتج هذا الاختبار أخطاء مثل هذه:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

ممارسة جيدة

على الرغم من أن الإعتراض الإضافي isNotNull ليس ضارًا حقًا، يجب تجنبه للأسباب التالية:

  • إنه لا يضيف أي قيمة إضافية. إنه مجرد المزيد من الكود للقراءة والصيانة.
  • يفشل الاختبار على أي حال عندما يكون service null ونرى السبب الجذري الحقيقي للفشل. لا يزال الاختبار يخدم هدفه.
  • حتى أن رسالة الخطأ الناتجة أفضل مع ادعاء AssertJ.

انظر إلى ادعاء الاختبار المعدل أدناه.

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

ينتج الاختبار المعدل خطأ كهذا:

Java

 

java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

ملاحظة: يمكن العثور على المثال في SimpleSpringTest.

ادعم القيم وليس النتيجة

من وقت لآخر، نكتب اختبارًا صحيحًا، لكن بطريقة “سيئة”. هذا يعني أن الاختبار يعمل تمامًا كما هو مقصود ويتحقق من مكوناتنا، لكن الفشل لا يوفر معلومات كافية. لذا، هدفنا هو ادعاء القيمة وليس نتيجة المقارنة.

ممارسة سيئة

دعونا نرى اثنين من هذه الاختبارات السيئة:

Java

 

// #1
assertThat(argument.contains("o")).isTrue();

// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();

// #3
assertThat("".isBlank()).isTrue();

// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();

يظهر بعض الأخطاء من الاختبارات أعلاه أدناه.

Plain Text

 

#1
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
  
#3
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

ممارسة جيدة

الحل بسيط جدًا مع AssertJ و API السلس الخاصة به. يمكن إعادة كتابة كل الحالات المذكورة أعلاه بسهولة:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

نفس الأخطاء المذكورة من قبل توفر الآن قيمة اكثر.

Plain Text

 

#1
Expecting actual:
  "Hello"
to contain:
  "f" 
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
    
#3
Expecting blank but was: "a"
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Note: The example can be found in SimpleParamTests.

جمع التوقيعات المتعلقة ببعضها البعض

تساعد سلسلة التوقيع ومسافة المسطرة المتعلقة بالكود كثيرًا في زيادة وضوح وقابلية قراءة الاختبار.

ممارسة سيئة

عندما نكتب اختبارًا ، قد ننتهي بالنتيجة الصحيحة ولكن أقل قابلية للقراءة. دعونا نتخيل اختبارًا نريد فيه العثور على دول وإجراء هذه الشيكات:

  1. عد الدول الموجودة.
  2. احتساب القيمة الأولى مع عدة قيم.

يمكن أن تبدو مثل هذا المثال:

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result).hasSize(5);
	var country = result.get(0);
	assertThat(country.getName()).isEqualTo("Spain");
	assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}

ممارسة جيدة

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

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result)
		.hasSize(5)
		.singleElement()
		.satisfies(c -> {
			assertThat(c.getName()).isEqualTo("Spain");
			assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
		});
}

Note: The example can be found in CountryRepositoryOtherTests.

منع الاختبارات الناجحة بشكل زائف

عند استخدام أي طريقة ادعاء تحتوي على وسيط ThrowingConsumer، يجب أن يحتوي الوسيط على assertThat داخل المستهدف أيضًا. وإلا ستجتاز الاختبار دائمًا حتى عندما يفشل المقارنة، مما يعني أن الاختبار غير صحيح. يفشل الاختبار فقط عندما يلقي ادعاء RuntimeException أو AssertionError استثنائي. أعتقد أن الأمر واضح، لكن من السهل نسيانه وكتابة الاختبار الخاطئ. يحدث لي هذا من وقت لآخر.

ممارسة سيئة

لنتخيل أن لدينا عدد من رموز الدول ونرغب في التحقق من أن كل رمز يفي بشرط معين. في حالتنا الوهمية، نريد التأكد من أن كل رمز دولة يحتوي على حرف “a”. كما ترون، هذا سخيف: لدينا رموز بالأحرف الكبيرة، لكننا لم نطبق الحساسية للحالة في الادعاء.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

ومن المستغرب أن تلك الاختبارات اجتازت بنجاح.

ممارسة جيدة

كما ذكرنا في بداية هذا القسم، يمكن تصحيح الاختبار بسهولة بإضافة assertThat داخل المستهدف (السطر 7). يجب أن يكون الاختبار الصحيح هو:

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

الآن يفشل الاختبار كما هو متوقع مع رسالة الخطأ الصحيحة.

Plain Text

 

java.lang.AssertionError: 
Expecting all elements of:
  ["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:

"CZ"
error: 
Expecting actual:
  "CZ"
to contain:
  "a"
 (ignoring case)
	at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)

سلاسل الادعاء

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

إدعاءات غير متشابكة

لنأخذ listLogs اختباراً، والذي يهدف لاختبار تسجيل مكون. الهدف هنا هو التحقق:

  • عدد السجلات المجمعة
  • اثبات وجود DEBUG و INFO للرسائل السجل
Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list ).hasSize(2);
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		});
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

تسلسل الادعاءات

باستخدام API السلس المذكور والتسلسل، يمكن تغيير الاختبار بهذه الطريقة:

Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list )
		.hasSize(2)
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		})
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

ملاحظة: يمكن العثور على المثال في AppleTest.

ملخص وكود المصدر

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

يمكن العثور على الكود المستخدم في مستودعاتي:

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj