Vodič za JUnit 5, 2. dio: Jedinstveno testiranje opružnog MVC-a s JUnit-om 5

Spring MVC jedan je od najpopularnijih Java okvira za izgradnju Java aplikacija za poduzeće i vrlo je pogodan za testiranje. Dizajn, Spring MVC potiče razdvajanje problema i potiče kodiranje na sučeljima. Te osobine, zajedno s Springovom primjenom ubrizgavanja ovisnosti, čine Spring aplikacije vrlo provjerljivima.

Ovaj je vodič druga polovica mog uvoda u jedinstveno testiranje s JUnit 5. Pokazat ću vam kako integrirati JUnit 5 s Springom, a zatim ću vas upoznati s tri alata koja možete koristiti za testiranje Spring MVC kontrolera, usluga i spremišta.

preuzimanje Preuzmite kod Preuzmite izvorni kod za primjere aplikacija korištenih u ovom vodiču. Stvorio Steven Haines za JavaWorld.

Integriranje JUnit 5 s oprugom 5

Za ovaj vodič koristimo Maven i Spring Boot, pa prvo što moramo učiniti je dodati ovisnost JUnit 5 u našu Maven POM datoteku:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Baš kao što smo to učinili u 1. dijelu, za ovaj ćemo primjer upotrijebiti Mockito. Dakle, trebat ćemo dodati JUnit 5 Mockito knjižnicu:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith i klasa SpringExtension

JUnit 5 definira sučelje proširenja putem kojeg se klase mogu integrirati s testovima JUnit u različitim fazama životnog ciklusa izvršenja. Proširenja možemo omogućiti dodavanjem @ExtendWithnapomene u naše testne klase i određivanjem klase proširenja za učitavanje. Proširenje tada može implementirati različita sučelja za povratni poziv, koja će se pozivati ​​tijekom životnog ciklusa testa: prije pokretanja svih testova, prije svakog testiranja, nakon svakog testiranja i nakon izvođenja svih testova.

Spring definira SpringExtensionklasu koja se pretplaćuje na obavijesti o životnom ciklusu JUnit 5 za stvaranje i održavanje "testnog konteksta". Prisjetimo se da kontekst aplikacije Spring sadrži sve grah Spring u aplikaciji i da izvodi ubrizgavanje ovisnosti kako bi povezao aplikaciju i njezine ovisnosti. Spring koristi model proširenja JUnit 5 za održavanje konteksta aplikacije testa, što pisanje jedinstvenih testova čini Spring jednostavnim.

Nakon što smo dodali JUnit 5 knjižnicu u našu Maven POM datoteku, možemo koristiti SpringExtension.classza proširenje naših JUnit 5 test klasa:

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

Primjer je, u ovom slučaju, aplikacija Spring Boot. Srećom @SpringBootTestnapomena već uključuje @ExtendWith(SpringExtension.class)napomenu, pa je trebamo samo uključiti @SpringBootTest.

Dodavanje ovisnosti Mockito

Da bismo ispravno testirali svaku komponentu u izolaciji i simulirali različite scenarije, htjet ćemo stvoriti lažne implementacije ovisnosti svake klase. Ovdje dolazi Mockito. U svoju POM datoteku uključite sljedeću ovisnost da biste dodali podršku za Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Nakon što integrirate JUnit 5 i Mockito u svoj Spring program, Mockito možete iskoristiti jednostavnim definiranjem Spring bean-a (poput usluge ili spremišta) u svojoj testnoj klasi pomoću @MockBeanbilješke. Evo našeg primjera:

 @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; ... } 

U ovom primjeru stvaramo lažni podatak WidgetRepositoryunutar našeg WidgetServiceTestrazreda. Kada Spring to vidi, automatski će ga povezati s našim, WidgetServicetako da možemo stvoriti različite scenarije u našim metodama ispitivanja. Svaka ispitna metoda konfigurirat će ponašanje WidgetRepository, na primjer vraćanjem traženog Widgetili vraćanjem Optional.empty()upita za koji podaci nisu pronađeni. Ostatak ovog tutorijala potrošit ćemo na primjere različitih načina konfiguriranja ovih lažnih graha.

Primjer proljetne MVC aplikacije

Da bismo napisali jedinstvene testove utemeljene na proljeću, potrebna nam je aplikacija protiv kojih ćemo ih napisati. Srećom, možemo se poslužiti primjerom iz mog vodiča za Spring Series "Ovladavanje Spring Spring Frameworkom, 1. dio: Spring MVC." Primjer aplikacije iz tog vodiča upotrijebio sam kao osnovnu aplikaciju. Izmijenio sam ga jačim REST API-jem kako bismo imali još nekoliko stvari za testiranje.

Primjer aplikacije je web aplikacija MVC Spring s REST kontrolerom, slojem usluge i spremištem koje koristi Spring Data JPA za zadržavanje "widgeta" u i iz H2 baze podataka u memoriji. Slika 1 je pregled.

Steven Haines

Što je widget?

A Widgetje samo "stvar" s ID-om, imenom, opisom i brojem verzije. U ovom je slučaju naš widget označen JPA bilješkama kako bi ga definirao kao entitet. WidgetRestControllerJe proljeće MVC kontroler koji prevesti miran API poziva na akcije obavljati na Widgets. Standardna WidgetServiceje usluga Spring koja definira poslovnu funkcionalnost za Widgets. Konačno, to WidgetRepositoryje Spring Data JPA sučelje, za koje će Spring stvoriti implementaciju u vrijeme izvođenja. Pregledat ćemo kôd za svaki razred dok ćemo pisati testove u sljedećim odjeljcima.

Jedinstveno testiranje proljetne usluge

Počnimo s pregledom kako testirati proljetnu  uslugu , jer je to najlakša komponenta u našoj MVC aplikaciji za testiranje. Primjeri u ovom odjeljku omogućit će nam da istražimo integraciju JUnit 5 s Springom bez uvođenja novih komponenata ili knjižnica za testiranje, iako ćemo to učiniti kasnije u vodiču.

Započet ćemo s pregledom WidgetServicesučelja i WidgetServiceImplklase, koji su prikazani na Popisu 1, odnosno Popisu 2.

Popis 1. Proljetno sučelje usluge (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); }

Popis 2. Klasa implementacije Spring usluge (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); } }

WidgetServiceImplje proljetna usluga, označena @Servicebilješkom, koja je WidgetRepositoryožičena kroz svoj konstruktor. U findById(), findAll()i deleteById()metode su svi prolaz metode za temeljni WidgetRepository. Jedina poslovna logika koju ćete pronaći nalazi se u save()metodi koja povećava broj verzije Widgetkada je spremljena.

Test klasa

Da bismo testirali ovu klasu, moramo stvoriti i konfigurirati mock WidgetRepository, povezati ga u WidgetServiceImplinstancu, a zatim povezati WidgetServiceImplu našu test klasu. Srećom, to je daleko lakše nego što zvuči. Popis 3 prikazuje izvorni kod za WidgetServiceTestklasu.

Popis 3. Probna klasa proljetne usluge (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"); } } 

WidgetServiceTestKlasa je označen s @SpringBootTestnaznakom koji skenira CLASSPATHza sve proljeće konfiguracije klasa i grah i postavlja proljetnom aplikacijski kontekst za test klase. Imajte na umu da WidgetServiceTestimplicitno uključuje i @ExtendWith(SpringExtension.class)napomenu putem @SpringBootTestnapomene, koja integrira test klasu s JUnit 5.

Test klasa također koristi Proljećevu @Autowiredbilješku za automatsko povezivanje a WidgetServiceza testiranje, a koristi Mockitovu @MockBeannapomenu za stvaranje lažne slike WidgetRepository. U ovom trenutku imamo maketu WidgetRepositorykoju možemo konfigurirati i stvarnu WidgetServices maketom WidgetRepositoryspojenom u nju.

Testiranje proljetne službe

Prva metoda ispitivanja ,, testFindById()izvršava metodu, koja bi trebala vratiti datoteku koja sadrži . Započinjemo sa stvaranjem onoga za koji se želimo da se vrati. Zatim koristimo Mockito API za konfiguriranje metode. Struktura naše lažne logike je sljedeća:WidgetServicefindById()OptionalWidgetWidgetWidgetRepositoryWidgetRepository::findById

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

U ovom slučaju kažemo: Vratite an Optionalof our Widgetkada se findById()metoda spremišta pozove s argumentom 1 (kao a long).

Dalje, zazivamo WidgetService„s findByIdmetodom s argumentom 1. Mi smo tada potvrditi da je prisutan i da se vrati Widgetonaj koji se konfigurira tobože WidgetRepositoryza povratak.