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' @ExtendWithannotazione 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 SpringExtensionclasse 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.classper estendere le nostre classi di test JUnit 5:

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

L'esempio, in questo caso, è un'applicazione Spring Boot. Fortunatamente l' @SpringBootTestannotazione 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' @MockBeanannotazione. 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 WidgetRepositoryall'interno della nostra WidgetServiceTestclasse. Quando Spring lo vedrà, lo collegherà automaticamente al nostro in WidgetServicemodo che possiamo creare diversi scenari nei nostri metodi di test. Ogni metodo di test configurerà il comportamento di WidgetRepository, ad esempio restituendo la richiesta Widgeto 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.

Steven Haines

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' WidgetServiceinterfaccia e la WidgetServiceImplclasse, 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' @Serviceannotazione, a cui è stato WidgetRepositorycollegato 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 Widgetquando viene salvato.

La classe di prova

Per testare questa classe, dobbiamo creare e configurare un mock WidgetRepository, collegarlo WidgetServiceImplall'istanza e quindi collegarlo alla WidgetServiceImplnostra classe di test. Fortunatamente, è molto più facile di quanto sembri. Il Listato 3 mostra il codice sorgente per la WidgetServiceTestclasse.

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 WidgetServiceil findById()metodo di, che dovrebbe restituire un file Optionalcontenente un file Widget. Iniziamo creando un'immagine Widgetche vogliamo WidgetRepositoryche ritorni. Quindi sfruttiamo l'API Mockito per configurare il WidgetRepository::findByIdmetodo. 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 Optionaldei nostri Widgetquando il findById()metodo del repository viene chiamato con un argomento di 1 (come a long).

Successivamente, invochiamo il metodo WidgetService's findByIdcon un argomento 1. Verifichiamo quindi che sia presente e che il valore restituito Widgetsia quello per cui abbiamo configurato il mock WidgetRepository.