מדריך JUnit 5, חלק 2: בדיקת יחידות MVC באביב עם JUnit 5

Spring MVC היא אחת ממסגרות Java הפופולריות ביותר לבניית יישומי Java ארגוניים, והיא מתאימה מאוד לבדיקה. לפי תכנון, אביב MVC מקדם הפרדה בין חששות ומעודד קידוד כנגד ממשקים. איכויות אלה, יחד עם יישום הזרקת התלות של אביב, הופכים את היישומים של אביב לבדיקת מאוד.

מדריך זה הוא המחצית השנייה של ההקדמה שלי לבדיקת יחידות עם JUnit 5. אני אראה לך כיצד לשלב את JUnit 5 עם Spring, ואז אציג בפניך שלושה כלים שתוכל להשתמש בהם לבדיקת בקרי MVC של Spring, שירותים ומאגרים.

הורד קבל את הקוד הורד את קוד המקור למשל יישומים המשמשים במדריך זה. נוצר על ידי סטיבן היינס עבור JavaWorld.

שילוב JUnit 5 עם Spring 5

לצורך הדרכה זו אנו משתמשים ב- Maven ו- Spring Boot, לכן הדבר הראשון שעלינו לעשות הוא להוסיף את תלות JUnit 5 לקובץ ה- POM של Maven שלנו:

  org.junit.jupiter junit-jupiter 5.6.0 test  

בדיוק כמו שעשינו בחלק 1, נשתמש במוקיטו לדוגמא זו. אז נצטרך להוסיף את ספריית JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith ושיעור SpringExtension

JUnit 5 מגדיר ממשק הרחבה , דרכו מחלקות יכולות להשתלב במבחני JUnit בשלבים שונים של מחזור החיים של הביצוע. אנו יכולים לאפשר הרחבות על ידי הוספת @ExtendWithההערה לשיעורי הבדיקה שלנו וציינו את מחלקת התוספות לטעינה. לאחר מכן הרחבה יכולה ליישם ממשקי התקשרות חוזרים שונים, אשר יופעלו לאורך כל מחזור חיי הבדיקה: לפני שכל הבדיקות פועלות, לפני שכל בדיקה פועלת, אחרי כל בדיקת הפעלה, ואחרי שכל הבדיקות רצו.

Spring מגדיר SpringExtensionמחלקה המנויים להודעות מחזור החיים של JUnit 5 כדי ליצור ולשמר "הקשר מבחן". כזכור, ההקשר של היישום של Spring מכיל את כל שעועית ה- Spring ביישום וכי הוא מבצע הזרקת תלות כדי לחבר יישום ותלותיו. Spring משתמש במודל ההרחבה JUnit 5 כדי לשמור על הקשר היישום של הבדיקה, מה שהופך את בדיקות היחידה לכתיבה עם Spring לפשוטות.

לאחר שהוספנו את ספריית JUnit 5 לקובץ MOM POM שלנו, נוכל להשתמש בה SpringExtension.classכדי להרחיב את שיעורי הבדיקה JUnit 5 שלנו:

 @ExtendWith(SpringExtension.class) class MyTests { // ... }

הדוגמה, במקרה זה, היא יישום Spring Boot. למרבה המזל @SpringBootTestההערה כבר כוללת את @ExtendWith(SpringExtension.class)ההערה, אז אנחנו צריכים רק לכלול @SpringBootTest.

הוספת התלות Mockito

כדי לבדוק כראוי כל רכיב בבידוד ולדמות תרחישים שונים, נרצה ליצור יישומים מדומים של התלות של כל מחלקה. כאן נכנס Mockito. כלול את התלות הבאה בקובץ POM שלך כדי להוסיף תמיכה ל- Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

לאחר ששילבתם את JUnit 5 ו- Mockito ביישום האביב שלכם, תוכלו למנף את Mockito על ידי הגדרת שעועית Spring (כגון שירות או מאגר) בכיתת הבדיקה שלכם באמצעות @MockBeanההערה. הנה הדוגמה שלנו:

 @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; ... } 

בדוגמה זו אנו יוצרים דמה WidgetRepositoryבתוך WidgetServiceTestהכיתה שלנו . כאשר האביב יראה זאת, הוא יחבר את זה אוטומטית לשלנו WidgetServiceכך שנוכל ליצור תרחישים שונים בשיטות הבדיקה שלנו. כל שיטת בדיקה תגדיר את התנהגות ה- WidgetRepository, כגון על ידי החזרת המבוקש Widgetאו החזרת Optional.empty()שאילתה שעבורה הנתונים לא נמצאים. אנו נבזבז את שארית הדרכה זו בבחינת דוגמאות של דרכים שונות להגדרת תצורה של שעועית מדומה.

היישום לדוגמא Spring MVC

כדי לכתוב מבחני יחידות מבוססות אביב, אנו זקוקים ליישום שכתוב נגדם. למרבה המזל, אנו יכולים להשתמש ביישום הדוגמא מהמדריך שלי בסדרת האביב שלי "Mastering Spring framework 5, Part 1: Spring MVC." השתמשתי ביישום הדוגמא מאותה הדרכה כיישום בסיס. שיניתי אותו באמצעות ממשק API REST חזק יותר, כך שיהיה לנו עוד כמה דברים לבדוק.

היישום לדוגמא הוא יישום אינטרנט של Spring MVC עם בקר REST, שכבת שירות ומאגר המשתמש ב- Spring Data JPA כדי להתמיד ב"ווידג'טים "אל מסד נתונים H2 בזיכרון וממנו. איור 1 הוא סקירה כללית.

סטיבן היינס

מה יישומון?

A Widgetהוא רק "דבר" עם מזהה, שם, תיאור ומספר גרסה. במקרה זה, הווידג'ט שלנו מסומן עם הערות JPA כדי להגדיר אותו כישות. WidgetRestControllerהוא בקר האביב MVC כי לתרגם שיחות API נינוחות לתוך פעולות לביצוע על Widgets. WidgetServiceהוא שירות אביב תקן מגדיר פונקציונאלי עסקי Widgets. לבסוף, WidgetRepositoryהוא ממשק JPA של Spring Data, שעבורו Spring ייצור יישום בזמן הריצה. אנו נבדוק את הקוד עבור כל שיעור בזמן שאנו כותבים מבחנים בחלקים הבאים.

יחידת בדיקת שירות אביב

נתחיל בבדיקה כיצד לבדוק שירות Spring  , מכיוון שזה המרכיב הקל ביותר בבדיקת MVC שלנו. דוגמאות בסעיף זה יאפשרו לנו לחקור את האינטגרציה של JUnit 5 עם Spring בלי להציג רכיבי בדיקה או ספריות חדשים, אם כי נעשה זאת בהמשך המדריך.

נתחיל בסקירת WidgetServiceהממשק WidgetServiceImplוהמחלקה המוצגים ברשימה 1 וברשימה 2 בהתאמה.

רישום 1. ממשק שירות האביב (WidgetService.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import java.util.List; import java.util.Optional; public interface WidgetService { Optional findById(Long id); List findAll(); Widget save(Widget widget); void deleteById(Long id); }

רישום 2. מחלקת הטמעת שירות האביב (WidgetServiceImpl.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Service public class WidgetServiceImpl implements WidgetService { private WidgetRepository repository; public WidgetServiceImpl(WidgetRepository repository) { this.repository = repository; } @Override public Optional findById(Long id) { return repository.findById(id); } @Override public List findAll() { return Lists.newArrayList(repository.findAll()); } @Override public Widget save(Widget widget) { // Increment the version number widget.setVersion(widget.getVersion()+1); // Save the widget to the repository return repository.save(widget); } @Override public void deleteById(Long id) { repository.deleteById(id); } }

WidgetServiceImplהוא שירות מעיין, המבואר עם @Serviceההערה, WidgetRepositoryהמחובר אליו באמצעות הבנאי שלו. findById(), findAll(), ו deleteById()השיטות הן כל שיטות התמסורת אל הבסיסי WidgetRepository. ההיגיון העסקי היחיד שתמצא נמצא save()בשיטה, שמגדילה את מספר הגרסה של Widgetהשמירה.

כיתת המבחן

כדי לבדוק מחלקה זו, עלינו ליצור ולהגדיר תצוגה מקדימה WidgetRepository, לחבר אותה WidgetServiceImplלמופע ואז לחבר אותה למחלקת WidgetServiceImplהבדיקה שלנו. למרבה המזל, זה הרבה יותר קל ממה שזה נשמע. רישום 3 מציג את קוד המקור של WidgetServiceTestהכיתה.

רישום 3. מחלקת מבחן שירות האביב (WidgetServiceTest.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.any; @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; @Test @DisplayName("Test findById Success") void testFindById() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(Optional.of(widget)).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found"); Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock"); } @Test @DisplayName("Test findById Not Found") void testFindByIdNotFound() { // Setup our mock repository doReturn(Optional.empty()).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found"); } @Test @DisplayName("Test findAll") void testFindAll() { // Setup our mock repository Widget widget1 = new Widget(1l, "Widget Name", "Description", 1); Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4); doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll(); // Execute the service call List widgets = service.findAll(); // Assert the response Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets"); } @Test @DisplayName("Test save widget") void testSave() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(widget).when(repository).save(any()); // Execute the service call Widget returnedWidget = service.save(widget); // Assert the response Assertions.assertNotNull(returnedWidget, "The saved widget should not be null"); Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented"); } } 

WidgetServiceTestהייצוגי מבואר עם @SpringBootTestהביאור, אשר סורק את CLASSPATHכל כיתות תצורת האביב והשעועית וערכות את הקשר יישום ספרינג עבור הכיתה במבחן. שים לב WidgetServiceTestשכולל באופן מרומז גם את @ExtendWith(SpringExtension.class)ההערה, דרך @SpringBootTestההערה, המשלבת את מחלקת המבחן עם JUnit 5.

כיתת הבדיקה משתמשת גם @Autowiredבהערה של ספרינג כדי לחווט אוטומטית WidgetServiceלניסוי, והיא משתמשת בהערה של מוקיטו @MockBeanליצירת דמה WidgetRepository. בשלב זה, יש לנו דגם WidgetRepositoryשנוכל להגדיר, ואמיתי WidgetServiceעם העגול WidgetRepositoryהמחובר לתוכו.

בודקים את שירות האביב

שיטת הבדיקה הראשונה testFindById(),, מבצעת WidgetServiceאת findById()השיטה, אשר אמורה להחזיר את Optionalהמכילה Widget. אנו מתחילים ביצירה של Widgetאותה אנו רוצים WidgetRepositoryשיחזיר. לאחר מכן אנו ממנפים את ה- API של Mockito כדי להגדיר את WidgetRepository::findByIdהשיטה. מבנה ההיגיון המדומה שלנו הוא כדלקמן:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

במקרה זה אנו אומרים: החזירו אחד Optionalמשלנו Widgetכאשר findById()נקראת שיטת המאגר עם טיעון 1 (כ- a long).

לאחר מכן, אנו קוראים לשיטה WidgetServiceשל findByIdעם טיעון 1. לאחר מכן אנו מאמתים שהיא קיימת ושהמוחזר Widgetהוא זה שהגדרנו ללעז WidgetRepositoryלהחזיר.