Tutorial su JUnit 5, parte 2: test dell'unità Spring MVC con JUnit 5
Spring MVC è uno dei framework Java più popolari per la creazione di applicazioni Java aziendali e si presta molto bene ai test. In base alla progettazione, Spring MVC promuove la separazione delle preoccupazioni e incoraggia la codifica contro le interfacce. Queste qualità, insieme all'implementazione di Spring dell'iniezione delle dipendenze, rendono le applicazioni Spring molto testabili.
Questo tutorial è la seconda metà della mia introduzione al test di unità con JUnit 5. Ti mostrerò come integrare JUnit 5 con Spring, quindi ti presenterò tre strumenti che puoi utilizzare per testare controller, servizi e repository Spring MVC.
download Ottieni il codice Scarica il codice sorgente per le applicazioni di esempio utilizzate in questo tutorial. Creato da Steven Haines per JavaWorld.Integrazione di JUnit 5 con Spring 5
Per questo tutorial, stiamo usando Maven e Spring Boot, quindi la prima cosa che dobbiamo fare è aggiungere la dipendenza JUnit 5 al nostro file POM Maven:
org.junit.jupiter junit-jupiter 5.6.0 test
Proprio come abbiamo fatto nella Parte 1, useremo Mockito per questo esempio. Quindi, avremo bisogno di aggiungere la libreria JUnit 5 Mockito:
org.mockito mockito-junit-jupiter 3.2.4 test
@ExtendWith e la classe SpringExtension
JUnit 5 definisce un'interfaccia di estensione , attraverso la quale le classi possono integrarsi con i test JUnit in varie fasi del ciclo di vita di esecuzione. Possiamo abilitare le estensioni aggiungendo l' @ExtendWith
annotazione alle nostre classi di test e specificando la classe di estensione da caricare. L'estensione può quindi implementare varie interfacce di callback, che verranno richiamate durante il ciclo di vita del test: prima dell'esecuzione di tutti i test, prima dell'esecuzione di ogni test, dopo l'esecuzione di ogni test e dopo che tutti i test sono stati eseguiti.
Spring definisce una SpringExtension
classe che sottoscrive le notifiche del ciclo di vita di JUnit 5 per creare e mantenere un "contesto di test". Ricorda che il contesto dell'applicazione di Spring contiene tutti i bean di Spring in un'applicazione e che esegue l'inserimento delle dipendenze per collegare un'applicazione e le sue dipendenze. Spring utilizza il modello di estensione JUnit 5 per mantenere il contesto dell'applicazione del test, il che semplifica la scrittura di unit test con Spring.
Dopo aver aggiunto la libreria JUnit 5 al nostro file Maven POM, possiamo utilizzare SpringExtension.class
per estendere le nostre classi di test JUnit 5:
@ExtendWith(SpringExtension.class) class MyTests { // ... }
L'esempio, in questo caso, è un'applicazione Spring Boot. Fortunatamente l' @SpringBootTest
annotazione include già l' @ExtendWith(SpringExtension.class)
annotazione, quindi dobbiamo solo includerla @SpringBootTest
.
Aggiunta della dipendenza Mockito
Per testare adeguatamente ogni componente in isolamento e simulare diversi scenari, vorremo creare implementazioni fittizie delle dipendenze di ciascuna classe. È qui che entra in gioco Mockito. Includere la seguente dipendenza nel file POM per aggiungere il supporto per Mockito:
org.mockito mockito-junit-jupiter 3.2.4 test
Dopo aver integrato JUnit 5 e Mockito nella tua applicazione Spring, puoi sfruttare Mockito semplicemente definendo un bean Spring (come un servizio o un repository) nella tua classe di test utilizzando l' @MockBean
annotazione. Ecco il nostro esempio:
@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; ... }
In questo esempio, creiamo una simulazione WidgetRepository
all'interno della nostra WidgetServiceTest
classe. Quando Spring lo vedrà, lo collegherà automaticamente al nostro in WidgetService
modo che possiamo creare diversi scenari nei nostri metodi di test. Ogni metodo di test configurerà il comportamento di WidgetRepository
, ad esempio restituendo la richiesta Widget
o restituendo una Optional.empty()
per una query per la quale i dati non vengono trovati. Passeremo il resto di questo tutorial esaminando esempi di vari modi per configurare questi finti bean.
L'applicazione di esempio Spring MVC
Per scrivere unit test Spring, abbiamo bisogno di un'applicazione per scriverli. Fortunatamente, possiamo usare l'applicazione di esempio dal mio tutorial Spring Series "Mastering Spring framework 5, Part 1: Spring MVC". Ho usato l'applicazione di esempio di quel tutorial come applicazione di base. L'ho modificato con un'API REST più potente in modo da avere qualche altra cosa da testare.
L'applicazione di esempio è un'applicazione Web Spring MVC con un controller REST, un livello di servizio e un repository che utilizza Spring Data JPA per rendere persistenti i "widget" da e verso un database in memoria H2. La figura 1 è una panoramica.

Cos'è un widget?
A Widget
è solo una "cosa" con un ID, nome, descrizione e numero di versione. In questo caso, il nostro widget è annotato con annotazioni JPA per definirlo come entità. La WidgetRestController
è un controller Spring MVC che traduce chiamate API RESTful in azioni da eseguire sul Widgets
. Il WidgetService
è un servizio Primavera standard che definisce la funzionalità di business per Widgets
. Infine, WidgetRepository
è un'interfaccia JPA Spring Data, per la quale Spring creerà un'implementazione in fase di runtime. Rivedremo il codice per ogni classe mentre scriviamo i test nelle sezioni successive.
Test di unità di un servizio primaverile
Iniziamo esaminando come testare un servizio Spring , perché è il componente più semplice da testare nella nostra applicazione MVC. Gli esempi in questa sezione ci permetteranno di esplorare l'integrazione di JUnit 5 con Spring senza introdurre nuovi componenti o librerie di test, anche se lo faremo più avanti nel tutorial.
Inizieremo esaminando l' WidgetService
interfaccia e la WidgetServiceImpl
classe, mostrate rispettivamente nel Listato 1 e nel Listato 2.
Listato 1. L'interfaccia del servizio Spring (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); }
Listato 2. La classe di implementazione del servizio Spring (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
è un servizio Spring, annotato con l' @Service
annotazione, a cui è stato WidgetRepository
collegato un collegamento tramite il suo costruttore. I findById()
, findAll()
e deleteById()
metodi sono tutti metodi passthrough alla sottostante WidgetRepository
. L'unica logica aziendale che troverai si trova nel save()
metodo, che incrementa il numero di versione di Widget
quando viene salvato.
La classe di prova
Per testare questa classe, dobbiamo creare e configurare un mock WidgetRepository
, collegarlo WidgetServiceImpl
all'istanza e quindi collegarlo alla WidgetServiceImpl
nostra classe di test. Fortunatamente, è molto più facile di quanto sembri. Il Listato 3 mostra il codice sorgente per la WidgetServiceTest
classe.
Listato 3. La classe di test del servizio Spring (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"); } }
The WidgetServiceTest
class is annotated with the @SpringBootTest
annotation, which scans the CLASSPATH
for all Spring configuration classes and beans and sets up the Spring application context for the test class. Note that WidgetServiceTest
also implicitly includes the @ExtendWith(SpringExtension.class)
annotation, through the @SpringBootTest
annotation, which integrates the test class with JUnit 5.
The test class also uses Spring's @Autowired
annotation to autowire a WidgetService
to test against, and it uses Mockito's @MockBean
annotation to create a mock WidgetRepository
. At this point, we have a mock WidgetRepository
that we can configure, and a real WidgetService
with the mock WidgetRepository
wired into it.
Testing the Spring service
Il primo metodo di test,, testFindById()
esegue WidgetService
il findById()
metodo di, che dovrebbe restituire un file Optional
contenente un file Widget
. Iniziamo creando un'immagine Widget
che vogliamo WidgetRepository
che ritorni. Quindi sfruttiamo l'API Mockito per configurare il WidgetRepository::findById
metodo. La struttura della nostra falsa logica è la seguente:
doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD
In questo caso, stiamo dicendo: restituisce uno Optional
dei nostri Widget
quando il findById()
metodo del repository viene chiamato con un argomento di 1 (come a long
).
Successivamente, invochiamo il metodo WidgetService
's findById
con un argomento 1. Verifichiamo quindi che sia presente e che il valore restituito Widget
sia quello per cui abbiamo configurato il mock WidgetRepository
.