Test con Spring MVC, Spring Data JPA e Mockito

Esistono moltissimi tool per gestire i casi di test e ovviamente questo per lo sviluppatore è positivo perchè ci permette di avere tanti diversi approcci per testare un’applicazione. Nell’articolo di oggi vedremo uno dei possibili approcci per realizzare dei test su applicazioni basate principalmente sui framework messi a disposizione da Spring.

La nostra applicazione tipo sarà suddivisa in maniera abbastanza classica in 3 layer: Controller, Service e DAO. L’applicazione d’esempio mostra una pagina di login e permette banalmente di verificare le credenziali, senza avere troppe funzionalità che tanto non sono utili per i fini dell’articolo. Vediamo quindi come sia possibile testare i diversi layer applicativi

 

DAO

Per la parte di accesso ai dati è stato utilizzato Spring Data JPA. L’unica tabella mappata dall’applicativo è UTENTE e il relativo entity si chiama UtenteEntity. Di seguito viene riportato il repository che si occupa di gestire l’entità relativa agli utenti

package com.javastaff.logintestapp.dao;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javastaff.logintestapp.entity.UtenteEntity;

public interface UtenteRepository extends 
      JpaRepository<UtenteEntity,String>,UtenteRepositoryCustom{
}

Oltre a questo repository, che ci permette di avere le funzionalità CRUD su UtenteEntity, abbiamo anche una classe che ci permette di definire ulteriori metodi da esporre

package com.javastaff.logintestapp.dao.impl;

import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import com.javastaff.logintestapp.dao.UtenteRepositoryCustom;
import com.javastaff.logintestapp.entity.UtenteEntity;

public class UtenteRepositoryImpl implements UtenteRepositoryCustom{
    
    @PersistenceContext
    private EntityManager em;
    
    public List<UtenteEntity> listaUtentiRangeDataRegistrazione(
          Date dataRegistrazioneInizio,Date dataRegistrazioneFine) {
          List<UtenteEntity> utenteList=em.createQuery(
             "select u from UtenteEntity u where "
                + "dataRegistrazione BETWEEN :dataRegistrazioneInizio "
                +"AND :dataRegistrazioneFine",UtenteEntity.class)
          .setParameter("dataRegistrazioneInizio", dataRegistrazioneInizio)
          .setParameter("dataRegistrazioneFine", dataRegistrazioneFine)
          .getResultList();
        return utenteList;
    }
}

In questo caso l’unico metodo aggiuntivo è listaUtentiRangeDataRegistrazione che ritorna appunto una lista di utenti i quali si sono registrati nell’intervallo di tempo specificato come parametro. Per effettuare il test della parte DAO possiamo utilizzare un database H2 che possiamo far partire attraverso la seguente classe di configurazione

package com.javastaff.logintestapp.test.dao;

import java.sql.SQLException;

import org.h2.tools.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;

/**
 * Classe di configurazione usata per il setup del database
 */

public class TestDataContextConfiguration{

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    @DependsOn("dataSource")
    public Server dataSourceTcpConnector() {
        try {
            return Server.createTcpServer();
        } catch (SQLException sqlException) {
            throw new RuntimeException(sqlException);
        }
    }
}

In questo modo abbiamo a disposizione un database H2 per i nostri test. Dobbiamo però anche creare le tabelle (e volendo popolarle) e questo possiamo farlo utilizzando una configurazione Spring che richiamerà l’esecuzione di alcuni script

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans.xsd
               http://www.springframework.org/schema/context
               http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/jdbc
            http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
            http://www.springframework.org/schema/data/jpa
            http://www.springframework.org/schema/data/jpa/spring-jpa-1.8.xsd">
    
    <jdbc:embedded-database type="H2" id="dataSource">
        <jdbc:script location="sql/create-tables.sql"/>
        <jdbc:script location="sql/load-tables.sql"/>
    </jdbc:embedded-database>
    
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="persistenceUnitName" value="utenteDB" />
        <property name="persistenceProviderClass"
                  value="org.hibernate.ejb.HibernatePersistence"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.ejb.entitymanager_factory_name">utenteEmf</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>  
                <prop key="hibernate.show_sql">false</prop>
                <prop key="hibernate.cache.use_query_cache">true</prop>
                <prop key="hibernate.cache.use_second_level_cache">true</prop>
                <prop key="hibernate.generate_statistics">false</prop>
            </props>
        </property>
        <property name="packagesToScan">
            <list>
                <value>com.javastaff.logintestapp.entity</value>
            </list>
        </property>
    </bean>
    
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
   
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>   
    
    <jpa:repositories base-package="com.javastaff.logintestapp.dao"
                      entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>
    

</beans>

In questa configurazione abbiamo richiamato 2 script per creare/popolare le tabelle e inoltre abbiamo istanziato tutti i bean che servono a Spring Data JPA per poter funzionare. Possiamo quindi ora passare al vero e proprio caso di test. Utilizzando un framework che ci fornisce già i metodi CRUD non sarebbe tanto sensato testarli, quindi verrà definito solo il caso di test relativo al metodo che recupera gli utenti in base all’intervallo di registrazione.

package com.javastaff.logintestapp.test.dao;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import com.javastaff.logintestapp.dao.UtenteRepository;
import com.javastaff.logintestapp.entity.UtenteEntity;


/**
 * Casi di test relativi a UtenteRepository
 * */

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { DaoConfigConfiguration.class, 
                                  TestDataContextConfiguration.class })
@Transactional
public class UtenteRepositoryTest {
    protected static Logger logger = Logger.getLogger(
      UtenteRepositoryTest.class);
    
    @Autowired
    UtenteRepository utenteRepository;
    
    UtenteEntity utente;
    Date dataRegistrazione1;
    Date dataRegistrazione2;
    Date dataRegistrazioneUtente;
    
    @Before
    public void setupData() throws ParseException {
        logger.info("Caricamento dati");
        dataRegistrazioneUtente=
            new SimpleDateFormat("dd/MM/yyyy").parse("10/10/2009");
        dataRegistrazione1=
            new SimpleDateFormat("dd/MM/yyyy").parse("01/01/2009");
        dataRegistrazione2=
            new SimpleDateFormat("dd/MM/yyyy").parse("01/01/2010");
        utente=new UtenteEntity();
        utente.setUsername("test");
        utente.setPassword("");
        utente.setDataRegistrazione(dataRegistrazioneUtente);
        utenteRepository.save(utente);
    }

    @Test
    public void testListaUtentiRangeDataRegistrazione() {
        logger.info("testListaUtentiRangeDataRegistrazione()");
        List<UtenteEntity> listaUtenti=
            utenteRepository.listaUtentiRangeDataRegistrazione(
            dataRegistrazione1, dataRegistrazione2);
        assertNotNull(listaUtenti);
        assertEquals(listaUtenti.size(), 1);
        assertEquals(listaUtenti.get(0), utente);
    }
    
    @After
    public void cleanData() {
        utenteRepository.delete(utente);
    }
}

Il caso di test riporta l’annotation @RunWith(SpringJUnit4ClassRunner.class) che ci permette di utilizzare delle feature aggiuntive a JUnit per effettuare i test, come potete trovare in maniera dettagliata nella documentazione ufficiale. Nel metodo setupData, annotato con @Before che quindi verrà eseguito prima dell’esecuzione del test, predisponiamo i dati che ci serviranno per questo e li salviamo sul database.

Abbiamo già visto che tramite gli script possiamo popolare la nostra base dati, ma dipende da come vogliamo gestire i dati di test. Una soluzione potrebbe essere quella di popolare tramite script le tabelle parametriche o comunque di amministrazione della nostra applicazione e poi nel singolo caso di test popolare quello di cui abbiamo bisogno. Nel metodo di test testListaUtentiRangeDataRegistrazione andiamo quindi a richiamare il repository invocando il metodo da testare ed effettuiamo delle verifiche tramite dei metodi d’utilità di JUnit assertEquals e assertNotNull.

 

Service

Lo strato Service della nostra applicazione è la classe LoginServiceImpl che si occupa di verificare le credenziali dell’utente

package com.javastaff.logintestapp.service.impl;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.xml.bind.annotation.adapters.HexBinaryAdapter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.javastaff.logintestapp.dao.UtenteRepository;
import com.javastaff.logintestapp.entity.UtenteEntity;
import com.javastaff.logintestapp.service.LoginService;

@Service
public class LoginServiceImpl implements LoginService{
    
    @Autowired
    UtenteRepository utenteRepository;
    
    public boolean autentica(String username, String password) {
        boolean verifica=false;
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] passwordDigest = md.digest(password.getBytes());
            String digest=new HexBinaryAdapter().marshal(passwordDigest);
            UtenteEntity utente=utenteRepository.findOne(username);
            if ((utente!=null) && (utente.getPassword().equals(digest)))
                verifica=true;
        }
        catch(NoSuchAlgorithmException ex) {
            ex.printStackTrace();
        }
        
        return verifica;
    }

}

Il test che si deve occupare di questo componente si troverà nella classe LoginServiceTest e dovrà testare LoginServiceImpl senza occuparsi dello strato DAO. Per questo motivo utilizzeremo Mockito, framework opensource che ci permette di creare dei Mock, ovvero degli oggetti che simulano il comportamento di altri, all’interno dei nostri casi di test.

Quindi nel caso dello strato Service utilizzeremo una configurazione classica Spring per istanziare qualsiasi cosa ci possa servire e una classe di configurazione come la seguente

package com.javastaff.logintestapp.test.service;

import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;

import com.javastaff.logintestapp.dao.UtenteRepository;
import com.javastaff.logintestapp.service.LoginService;
import com.javastaff.logintestapp.service.impl.LoginServiceImpl;

/**
 * Classe di configurazione usata per i test relativi allo strato Service
 * */

@ImportResource("spring/applicationContext.xml")
public class ServiceConfigConfiguration {
    @Bean
    public LoginService utenteService() {
        return new LoginServiceImpl();
    }
    
    @Bean
    public UtenteRepository utenteRepository() {
        return Mockito.mock(UtenteRepository.class);
    }
}

Come potete vedere per UtenteRepository creiamo un Mock utilizzando Mockito, al quale passiamo semplicemente la classe di cui si deve occupare. All’interno del setup del caso di test invece andremo a definire come si deve comportare questo Mock quando viene richiamato

package com.javastaff.logintestapp.test.service;

import static org.junit.Assert.assertEquals;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.javastaff.logintestapp.dao.UtenteRepository;
import com.javastaff.logintestapp.entity.UtenteEntity;
import com.javastaff.logintestapp.service.LoginService;

/**
 * Casi di test relativi a LoginService
 * */

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ServiceConfigConfiguration.class})
public class LoginServiceTest {
    
    protected static Logger logger = 
         Logger.getLogger(LoginServiceTest.class);
    
    @Autowired
    LoginService loginService;
    
    @Autowired
    UtenteRepository utenteRepository;
    
    UtenteEntity utente;
    
    @Before
    public void setup() throws ParseException {
        logger.info("Caricamento dati");
        Date dataRegistrazione=
            new SimpleDateFormat("dd/MM/yyyy").parse("16/11/2016");
        utente=new UtenteEntity();
        utente.setUsername("utente");
        utente.setPassword("5F4DCC3B5AA765D61D8327DEB882CF99");
        utente.setDataRegistrazione(dataRegistrazione);
        Mockito.when(utenteRepository.findOne("utente")).thenReturn(utente);
    }
    
    @Test
    public void testAutenticazione(){
        logger.info("testAutenticazione");
        boolean autenticazione=
            loginService.autentica(utente.getUsername(), "password");
        assertEquals(true,autenticazione);
    }
    
    @Test
    public void testAutenticazioneFallita(){
        logger.info("testAutenticazioneFallita");
        boolean autenticazione=
            loginService.autentica(utente.getUsername(), "CIAO!");
        assertEquals(false,autenticazione);
    }
}

Anche qui abbiamo un metodo annotato con @Before che prepara i dati e in questo caso dice al Mock di UtenteRepository che quando verrà richiamato il suo metodo findOne, con il parametro String “utente” allora dovrà restituire l’entity utente appena costruita. I due test poi che sono presenti richiamano il metodo autentica di LoginService, prima con le credenziali corrette e poi con quelle sbagliate.

 

Controller

Vediamo infine come effettuare il test dello strato relativo ai Controller. In questo caso utilizzeremo Mockito per simulare lo strato Service e Spring MVC Test che fornisce una serie di utility che permettono di effettuare i test in maniera abbastanza agevole. Il Controller che dobbiamo testare mostra prima la pagina di login e poi richiama il metodo autentica di LoginService per controllare l’autenticazione.

package com.javastaff.logintestapp.controller;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.javastaff.logintestapp.form.LoginForm;
import com.javastaff.logintestapp.service.LoginService;

@Controller
@RequestMapping("/login")
public class LoginController {
    
    @Autowired
    LoginService loginService;
    
    @RequestMapping(method = RequestMethod.GET)
    public String mostraLogin(Model model) {
        model.addAttribute("loginForm", new LoginForm());
        return "login";
    }
    
    @RequestMapping(method = RequestMethod.POST)
    public String effettuaLogin(@Valid @ModelAttribute LoginForm loginForm,
         BindingResult result,Model model) {
        if (result.hasErrors())
            return "login";
        else {
            boolean autenticazioneEffettuata =
                  loginService.autentica(loginForm.getUsername(),
                  loginForm.getPassword());
            if (autenticazioneEffettuata)
                return "loginEffettuato";
            else {
                model.addAttribute("errorMessage", 
                  "Autenticazione non riuscita");
                return "login";
            }    
        }
    }
}

Il test in questo caso utilizzerà una configurazione simile a quella vista per lo strato Service, solo che i Mock adesso saranno i Service richiamati e le implementazioni da testare i Controller.

package com.javastaff.logintestapp.test.controller;

import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;

import com.javastaff.logintestapp.controller.LoginController;
import com.javastaff.logintestapp.service.LoginService;

/**
 * Classe di configurazione usata per i test 
 * relativi allo strato Controller
 * */

@ImportResource({"spring/applicationContext.xml",
                 "spring/spring-mvc-servlet.xml"})
public class ControllerConfigConfiguration {
    @Bean
    public LoginService loginService() {
        return Mockito.mock(LoginService.class);
    }
    
    @Bean
    public LoginController loginController() {
        return new LoginController();
    }
}

Quindi vediamo la classe di test LoginControllerTest che in fase di setup definisce un utente di prova e le risposte che dovrà fornire il metodo autentica di LoginService attraverso Mockito

package com.javastaff.logintestapp.test.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.javastaff.logintestapp.controller.LoginController;
import com.javastaff.logintestapp.entity.UtenteEntity;
import com.javastaff.logintestapp.service.LoginService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ControllerConfigConfiguration.class})
public class LoginControllerTest {
    protected static Logger logger = 
         Logger.getLogger(LoginControllerTest.class);
    
    @Autowired
    LoginService loginService;
    
    @Autowired
    LoginController loginController;
    
    UtenteEntity utente;
    String password="password";
    String passwordSbagliata="sbagliata";
    
    @Before
    public void setup() throws Exception{
        Date dataRegistrazione=
            new SimpleDateFormat("dd/MM/yyyy").parse("16/11/2016");
        utente=new UtenteEntity();
        utente.setUsername("utente");
        utente.setPassword("5F4DCC3B5AA765D61D8327DEB882CF99");
        utente.setDataRegistrazione(dataRegistrazione);
        Mockito.when(this.loginService.autentica(
            utente.getUsername(), password)).thenReturn(true);
        Mockito.when(this.loginService.autentica(
            utente.getUsername(), passwordSbagliata)).thenReturn(false);
    }
    
    @Test
    public void testEffettuaLogin() throws Exception {
        logger.info("testEffettuaLogin()");
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
            this.loginController).build();
        mockMvc.perform(post("/login/")
                .param("username", utente.getUsername())
                .param("password", password))
                .andExpect(status().isOk())
                .andExpect(view().name("loginEffettuato"));
    }
    
    @Test
    public void testEffettuaLoginErrata() throws Exception {
        logger.info("testEffettuaLoginErrata()");
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
            this.loginController).build();
        mockMvc.perform(post("/login/")
                .param("username", utente.getUsername())
                .param("password", passwordSbagliata))
                .andExpect(model().attributeExists("errorMessage"))
                .andExpect(status().isOk())
                .andExpect(view().name("login"));
    }
    
    @Test
    public void testMostraLogin() throws Exception {
        logger.info("testMostraLogin()");
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
            this.loginController).build();
        mockMvc.perform(get("/login/"))
                .andExpect(status().isOk())
                .andExpect(view().name("login"));
    }
}

I metodi di test utilizzano la classe MockMvc per descrivere il comportamento che dovrà avere il nostro caso di test. Utilizzando il pattern Builder vediamo che definiamo il path e il metodo con cui fare la richiesta,

mockMvc.perform(post("/login/")

successivamente i parametri

.param("username", utente.getUsername())
.param("password", password))

e cosa ci aspettiamo dal risultato

.andExpect(status().isOk())
.andExpect(view().name("loginEffettuato"));

 

Conclusioni

Quella che abbiamo visto in questo articolo è solo una possibile configurazione che si può utilizzare per scrivere dei test unitari per la nostra applicazione. Inoltre cercando nelle librerie di test possiamo trovare molte altre cose interessanti, come Selenium per i test d’integrazione (che abbiamo visto in questo e quest’altro articolo ), Cucumber per definire dei test Behavior Driven e tante altre interessanti iniziative. Di seguito trovate il progetto d’esempio dell’articolo presente su GitHub

Federico Paparoni

Looking for a right "about me"...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.