בדיקת יחידות הפכה לחלק סטנדרטי של הפיתוח. יש להשתמש בהרבה כלים לכך בדרכים שונות. מאמר זה מדגים כמה רמזים או, נאמר, יעדים טובים שעבדו עבורי.
במאמר זה, תלמד
- איך לכתוב בדיקות יחידה נקיות וקריאות עם JUnit וממשקי ההגבלה
- איך להימנע מבדיקות חיוביות שקריות במקרים מסוימים
- מה להימנע מכשירת יחידות
אל תשתמש בבדיקות NPE יתר
כולנו נוטים להימנע מNullPointerException
ככל האפשר בקוד הראשי כיוון שזה יכול להוביל לתוצאות מגוחכות. אני מאמין שהדאגה העיקרית שלנו אינה להימנע מ-NPE בבדיקות. המטרה שלנו היא לאמת את ההתנהגות של הרכיב הנבדק בדרך שקן, קריא ואמין.
פרקטיקה רעה
פעמים רבות בעבר, השתמשתי בהגבלה isNotNull
אפילו כשזה לא נחוץ, כמו בדוגמה למטה:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
בדיקה זו מייצרת שגיאות כאלה:
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.
ראה את ההצהרה המבוססת בדיקה המותאמת למטה.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
הבדיקה המותאמת מייצרת שגיאה כזו:
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.
טענת ערכים ולא התוצאה
מדי פעם אנו כותבים בדיקה נכונה, אך בצורה "רעה". זה אומר שהבדיקה פועלת בדיוק כפי שנועדה ומאשרת את הרכיב שלנו, אך הכשל אינו מספק מספיק מידע. לכן, המטרה שלנו היא לטעון את הערך ולא את תוצאת ההשוואה.
פרקטיקה רעה
בואו נראה כמה בדיקות רעות כאלה:
// #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();
חלק מהשגיאות מהבדיקות לעיל מוצגות להלן.
#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 הזכות שלו. כל המקרים שהוזכרו לעיל יכולים להיכתב בקלות כך:
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
אותן שגיאות שציינו קודם מספקות ערך רב יותר עכשיו.
#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)
הערה: דוגמא זו ניתן למצוא ב-SimpleParamTests.
אסוף הצהרות קשורות ביחד
שילוב ההצהרות ומתונת קידוד הקוד הקשורה מאוד עוזרת בבהירות וקריאיות הבדיקה.
ניסיון רע
כשאנו כותבים בדיקה, עשויים להגיע לבדיקה תקינה, אך פחות קריאה. נניח בדיקה שבה אנו רוצים למצוא מדינות ולבצע בדיקות אלה:
- לספור את המדינות שנמצאו.
- לבטוח בכניסה הראשונה עם מספר ערכים.
בדיקות כאלה יכולות להיראות כמו הדוגמא הזו:
@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
פעם אחת ולכתוב הצהרות מקושרות רבות כצורך. ראה את הגרסה המותאמת למטרה למטה.
@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");
});
}
הערה: דוגמא זו ניתן למצוא ב-CountryRepositoryOtherTests.
מניעת בדיקה מוצלחת כוזבת
כשמשתמשים בשיטת הטענה כלשהי עם טיעון ThrowingConsumer
, אז הטיעון חייב להכיל גם את assertThat
בתוך הצרכן. אחרת, הבדיקה תעבור תמיד – אפילו כשההשוואה נכשלת, מה שאומר בדיקה שגויה. הבדיקה תיכשל רק כשהטענה זורקת יוצא שגיאה של RuntimeException
או AssertionError
. אני מניח שזה ברור, אבל קל לשכוח את זה ולכתוב בדיקה שגויה. זה קורה לי מדי פעם.
פרקטיקה גרועה
�נניח שיש לנו מספר קודי מדינה ואנו רוצים לאמת שכל קוד מקיים תנאים מסוימים. במקרה השדות שלנו, אנו רוצים לטעון שכל קוד מדינה מכיל את התו "a". כפי שאפשר לראות, זה שטותי: יש לנו קודים באותיות גדולות, אך אנו לא מחברים חוסר רגישות לגובה בטענה.
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> countryCode.contains("a"));
}
באופן מפתיע, הבדיקה שלנו עברה בהצלחה.
פרקטיקה טובה
כפי שציינו בתחילת הסעיף, ניתן לתקן את הבדיקה שלנו בקלות עם הוספת assertThat
בצרכן (שורה 7). הבדיקה הנכונה צריכה להיות כזו:
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}
כעת הבדיקה נכשלת כצפוי עם הודעת השגיאה הנכונה.
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)
שרשור טענות
הרעיון האחרון אינו באמת פרקטיקה, אלא המלצה. ׄAPI הזורמי של AssertJ צריך להשתמש כדי ליצור בדיקות קריאות יותר.
טענות לא משולבות
נבחן את בדיקת listLogs
, שמטרתה לבדוק את התיעוד של רכיב. המטרה כאן היא לבדוק:
- מספר מוכרז של יצירת רישומים נאסף
- אשרור קיומו של
DEBUG
ו-INFO
מסרי יומן
@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 הזורם שהוזכר והקישור, אנו יכולים לשנות את הבדיקה כך:
@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