Tutoriel sur les tests JUnit4 avec Spring

Régis POUILLER

Cet article présente les tests unitaires JUnit4 avec Spring.

Les cas de tests présentés sont sur des classes sans accès à une base de données et sur des classes nécessitant un accès à une base de données.

Une discussion a été ouverte pour les commentaires sur la publication de cet article : [ Commentez ]

- Titre : Tutoriel sur les tests JUnit4 avec Spring

- Auteur : Régis POUILLER

- Parution : 8 octobre 2014

- Licence : Copyright ® 2014 Régis POUILLER. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Objectif

- I-B. Prérequis

- I-C. Ressources complémentaires

- I-D. Versions des logiciels

- II. Tests unitaires sans base de données

- II-A. Test d'une classe autonome simple

- II-A-1. Cas à tester

- II-A-2. Test

- II-B. Test d'une classe dépendant d'une autre classe

- II-B-1. Cas à tester

- II-B-2. Test

- III. Tests unitaires avec base de données

- III-A. Tests avec consultation dans une base de données

- III-A-1. Cas à tester

- III-A-2. Test avec Spring DbUnit

- III-A-3. Test avec Spring Test DbUnit

- III-B. Tests avec modifications dans une base de données

- III-B-1. Cas à tester

- III-B-2. Test avec Spring DbUnit

- III-B-3. Test avec Spring Test DbUnit

- III-C. Comparaison de Spring DbUnit et Spring Test DbUnit

- III-D. Autres solutions (alternatives)

- IV. Conclusion

- V. Remerciements

I - Introduction

I-A - Objectif

L'objectif de cet article est d'avoir un aperçu des tests unitaires JUnit4 (grâce aux annotations) avec Spring.

En premier, nous verrons des tests sans lien avec une base de données dans le cas d'une classe autonome (sans dépendance) et dans le cas d'une classe dépendant d'une autre classe (avec nécessité d'utilisation d'un simulacre (ou mock en Anglais)).

Dans un deuxième temps, nous verrons comment effectuer des tests sur des classes accédant à une base de données (en lecture et puis en écriture de données) grâce aux solutions Spring DbUnit et Spring Test DbUnit.

I-B - Prérequis

Il est intéressant d'avoir des notions sur les tests unitaires en Java et les tests DbUnit pour bien comprendre cet article.

I-C - Ressources complémentaires

Sur les tests unitaires avec JUnit :

- « Conception de tests unitaires avec JUnit » de Romain Guy.

Sur les tests unitaires avec JUnit4 :

- « Tests unitaires automatisés avec JUnit4 ».

Sur les simulacres :

- « Bouchon (stub) ou Simulacre (mock) ? » de Mathilde Lemée ;

- « Simulacres de tests avec EasyMock et JUnit 4 » de Baptiste Wicht ;

- « Tests unitaires et doublures de tests  : les simulacres ne sont pas des bouchons » de Martin Fowler et Bruno Orsier.

Sur HSQLDB :

- « Présentation et utilisation d'HSQLDB » de Baptiste Wicht.

I-D - Versions des logiciels

Les versions utilisées dans cet article sont :

- JUnit : 4.11 ;

- Spring : 4.1.0 ;

- Spring DbUnit : 1.4.0 ;

- Spring Test DbUnit : 1.1.0.

II - Tests unitaires sans base de données

II-A - Test d'une classe autonome simple

II-A-1 - Cas à tester

Dans un premier temps, nous allons tester une classe simple nommée « CasSimple ». Cette dernière implémente l'interface « ICasSimple » qui comporte une seule méthode.

Le diagramme de classes est présenté ci-dessous :

Les codes de la classe « CasSimple » et de l'interface « ICasSimple » sont présentés ci-dessous :

ICasSimple.java

package com.developpez.rpouiller.springtest;

public interface ICasSimple {

int additionner(final int param1, final int param2);
}

CasSimple.java

package com.developpez.rpouiller.springtest;

import org.springframework.stereotype.Component;

@Component
public class CasSimple implements ICasSimple {

@Override
public int additionner(final int param1, final int param2) {
return param1 + param2;
}
}

L'annotation @Component déclare la classe en tant que composant Spring. Au lancement, Spring parcourt le « classpath » afin d'identifier ses composants. Un composant Spring est éligible à l'injection en cas de configuration par annotations (c'est-à-dire quand « component-scan » est présent dans le fichier de configuration) et que le composant est compris dans le scope de « component-scan ».

Pour que la compilation puisse s'effectuer, il est nécessaire d'ajouter la dépendance « spring-context » dans le fichier « pom.xml ». En effet, c'est « spring-context » qui contient l'annotation @Component (depuis Spring 2.5).

Extrait du fichier « pom.xml »

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>

Cela donne l'arbre de dépendances suivant :

Extrait du résultat de la commande « mvn dependency:tree »

[INFO] com.developpez.rpouiller:spring-test-article:jar:0.0.1-SNAPSHOT
[INFO] \- org.springframework:spring-context:jar:4.1.0.RELEASE:compile
[INFO]    +- org.springframework:spring-aop:jar:4.1.0.RELEASE:compile
[INFO]    |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO]    +- org.springframework:spring-beans:jar:4.1.0.RELEASE:compile
[INFO]    +- org.springframework:spring-core:jar:4.1.0.RELEASE:compile
[INFO]    |  \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO]    \- org.springframework:spring-expression:jar:4.1.0.RELEASE:compile

II-A-2 - Test

Le fichier de configuration Spring ci-dessous, associé à ce test, ne contient que la déclaration du bean à tester.

CasSimpleTest-context.xml

<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

<bean id="casSimple" class="com.developpez.rpouiller.springtest.CasSimple" />

</beans>

Le test de « CasSimple » ci-dessous vérifie le résultat de la méthode « additionner ».

CasSimpleTest.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.assertEquals;

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.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners(DependencyInjectionTestExecutionListener.class)
public class CasSimpleTest {

@Autowired
private ICasSimple casSimple;

@Test
public void testAdditionner() {
// Arrange
final int param1 = 2;
final int param2 = 2;
final int expected = 4;

// Act
final int resultat = casSimple.additionner(param1, param2);

// Assert
assertEquals(expected, resultat);
}
}

Les annotations @Test et @RunWith appartiennent à JUnit4.

Il y a plus d'informations sur ces annotations dans l'article :

- « Tests unitaires automatisés avec JUnit4 ».

L'annotation @Autowired (depuis Spring 2.5) est contenue dans « spring-context ». Elle indique à Spring de réaliser automatiquement l'injection de dépendances.

L'annotation @ContextConfiguration (depuis Spring 2.5) est contenue dans « spring-test ». Elle indique comment charger et configurer l'ApplicationContext. Par défaut, le fichier de configuration est contenu dans le même dossier (au niveau « classpath », donc potentiellement dans un autre dossier de sources) et son nom est de la forme « NomDeLaClasseDeTest-context.xml » (dans le cas de ce test, cela donne « CasSimpleTest-context.xml ».

L'annotation @TestExecutionListeners (depuis Spring 2.5) est également contenue dans « spring-context ». Cette annotation indique les classes (sous-classes) TestExecutionListener liées à cette classe de test. Les « listeners » jouent le rôle des annotations Before et After : ils contiennent des traitements à effectuer avant et après un test sans qu'il y ait besoin de les définir à chaque classe de test.

DependencyInjectionTestExecutionListener (depuis Spring 2.5) a la charge de l'injection de dépendances.
Cependant, la ligne « @TestExecutionListeners(DependencyInjectionTestExecutionListener.class) » ci-dessus est facultative parce que DependencyInjectionTestExecutionListener est par défaut avec SpringJUnit4ClassRunner.

La classe SpringJUnit4ClassRunner (depuis Spring 2.5) est contenue dans « spring-test ». Elle est le Runner (un Runner exécute un test JUnit) pour les tests avec Spring.

Il faut donc rajouter les dépendances « spring-test » et « junit » dans le fichier « pom.xml » :

Extrait du fichier « pom.xml »

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

Avec ces deux nouvelles dépendances, cela donne l'arbre de dépendances suivant :

Extrait du résultat de la commande « mvn dependency:tree »

[INFO] com.developpez.rpouiller:spring-test-article:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework:spring-context:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] |  +- org.springframework:spring-beans:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-core:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] |  \- org.springframework:spring-expression:jar:4.1.0.RELEASE:compile
[INFO] +- org.springframework:spring-test:jar:4.1.0.RELEASE:test
[INFO] \- junit:junit:jar:4.11:test
[INFO]    \- org.hamcrest:hamcrest-core:jar:1.3:test

II-B - Test d'une classe dépendant d'une autre classe

II-B-1 - Cas à tester

Nous allons maintenant voir le test d'une classe dépendant d'une autre classe. C'est un cas courant où nous allons devoir utiliser un objet simulacre.

Pour en savoir plus sur les simulacres, vous pouvez lire les articles :

- « Bouchon (stub) ou Simulacre (mock) ? » de Mathilde Lemée ;

- « Simulacres de tests avec EasyMock et JUnit 4 » de Baptiste Wicht ;

- « Tests unitaires et doublures de tests : les simulacres ne sont pas des bouchons » de Martin Fowler et Bruno Orsier.

Dans cet exemple la classe « ServiceSimple » implémentant l'interface « IServiceSimple » utilise la classe « DaoSimple » qui elle-même implémente l'interface « IDaoSimple ».

Le diagramme de classes est présenté ci-dessous :

Les codes de la classe « ServiceSimple » et des interfaces « IServiceSimple » et « IDaoSimple » sont présentés ci-dessous :

IServiceSimple.java

package com.developpez.rpouiller.springtest;

public interface IServiceSimple {

int compter(final String critere);
}

IDaoSimple.java

package com.developpez.rpouiller.springtest;

public interface IDaoSimple {

int compter(final String critere);
}

Dans cet exemple simple, les interfaces de service et de dao sont similaires.

ServiceSimple.java

package com.developpez.rpouiller.springtest;

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

@Service
public class ServiceSimple implements IServiceSimple {

@Autowired
private IDaoSimple dao;

public void setDao(final IDaoSimple dao) {
this.dao = dao;
}

@Override
public int compter(final String critere) {
if (critere == null) {
throw new NullPointerException("Le critère ne doit pas être null");
}
final int valeur = dao.compter(critere);
return valeur;
}
}

L'annotation @Service (depuis Spring 2.5) est contenue dans « spring-context ». Elle est une spécialisation de l'annotation @Component. Elle indique que la classe est un service. Elle peut être remplacée par l'annotation (en fait @Service est annotée avec @Component), mais @Service améliore la lisibilité du code en indiquant le rôle de la classe.

La classe « DaoSimple », implémentant l'interface « IDaoSimple », n'est pas présentée, car elle ne présente par d'intérêt dans cet test.

II-B-2 - Test

Le simulacre « DaoSimpleMock » ci-dessous implémente l'interface « IDaoSimple ». Il restitue la valeur du paramètre « pCritere » grâce à la méthode « getCritere() » et lève un échec de test si la méthode est appelée plus d'une fois.

DaoSimpleMock.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.fail;

public class DaoSimpleMock implements IDaoSimple {

private String critere;

@Override
public int compter(final String critere) {
if (this.critere != null) {
fail("Le service ne doit appeler la DAO qu'une seule fois");
}
this.critere = critere;
return 10;
}

public String getCritere() {
return critere;
}

public void initMock() {
critere = null;
}
}

Dans le fichier de configuration ci-dessous, on remarque l'injection du simulacre ci-dessus dans le service par configuration.

contextDifferent.xml

<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

<bean id="mock" class="com.developpez.rpouiller.springtest.DaoSimpleMock" scope="singleton" />
<bean id="service" class="com.developpez.rpouiller.springtest.ServiceSimple" scope="singleton">
<property name="dao" ref="mock" />
</bean>
</beans>

Le test ci-dessous vérifie que le service renvoie la valeur retournée par la dao et que cette dernière a bien reçu le paramètre fourni au service.

ServiceSimpleTest.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="contextDifferent.xml")
public class ServiceSimpleTest {

@Autowired
private IServiceSimple service;
@Autowired
@Qualifier(value="mock")
private DaoSimpleMock dao;

@Before
public void initTest() {
dao.initMock();
}

@Test
public void testCompterCrit() {
// Arrange
final String critere = "Crit";
final int expected = 10;

// Act
final int valeur = service.compter(critere);

// Assert
assertEquals(expected, valeur);
assertEquals(critere, dao.getCritere());
}

@Test
public void testCompterCrit2() {
// Arrange
final String critere = "Crit2";
final int expected = 10;

// Act
final int valeur = service.compter(critere);

// Assert
assertEquals(expected, valeur);
assertEquals(critere, dao.getCritere());
}
}

L'annotation @Qualifier (depuis Spring 2.5) est contenue dans « spring-beans ». Elle permet d'indiquer quelle instance doit être injectée. Dans le cas présent, cela nous permet de récupérer la dao (qui est un singleton) et de vérifier la valeur du paramètre reçu par la méthode « compter(final String critere) ».

On peut remarquer l'absence de l'annotation @TestExecutionListeners, car le seul TestExecutionListener nécessaire est DependencyInjectionTestExecutionListener, et comme indiqué dans le chapitre II-A-2. Test, il est inclus par défaut avec SpringJUnit4ClassRunner.

On peut également remarquer que l'annotation @ContextConfiguration a un paramètre locationslocations qui permet d'indiquer un fichier de configuration différent de celui par défaut. En fait, permet d'indiquer un tableau de fichiers de configuration qui se cumulent.

III - Tests unitaires avec base de données

Pour les besoins de ce tutoriel, nous utiliserons la base de données HSQLDB (HyperSQL DataBase).

Pour avoir une présentation de HSQLDB, vous pouvez lire l'article :

- « Présentation et utilisation d'HSQLDB » de Baptiste Wicht.

Il faut donc rajouter la dépendance Maven suivante :

Extrait du fichier « pom.xml »

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.3.2</version>
</dependency>

Voici la table qui sera utilisée en exemple dans ce tutoriel :

Voici son script SQL de création :

Script SQL de création de la table « EXEMPLE »

CREATE TABLE EXEMPLE (
IDENTIFIANT INT PRIMARY KEY,
NOM VARCHAR(50) NOT NULL
);

Pour lancer le gestionnaire de base HSQLDB, exécutez la classe « org.hsqldb.util.DatabaseManager ». Voici la configuration pour la connexion à la base de données d'exemple (ici avec l'URL « jdbc:hsqldb:file:base/exemple ») :

Voici le résultat de la création de la table :

Les implémentations des dao seront réalisées en utilisant JdbcTemplate.

Pour effectuer des tests sur une base de données avec JUnit (sans Spring), nous utiliserons DbUnit.

Pour lier Spring à DbUnit, nous pouvons utiliser Spring DbUnit ou Spring Test DbUnit.

Nous utiliserons la version 1.4.0 de Spring DbUnit qui est compatible avec Spring 4.0.0.
Pour une version de Spring antérieure (3.2.7 par exemple), il faudrait utiliser une version de Spring DbUnit antérieure à la 1.2.0 (1.1.12, par exemple).

Nous utiliserons la version 1.1.0 de Spring Test DbUnit qui est compatible avec Spring 4.0.0.
Pour une version de Spring antérieure, il faudrait utiliser la version 1.0.1.

La raison de la compatibilité/incompatibilité de certaines versions est un changement dans Spring : la classe TestContext est devenue l'interface TestContext.

III-A - Tests avec consultation dans une base de données

Pour voir les tests avec une base de données, nous allons tout d'abord voir des tests sur des opérations de lecture. Il sera donc juste nécessaire de charger des informations dans la base de données. Puis le test consistera à vérifier que la lecture retourne bien le résultat attendu.

Nous verrons les annotations pour le chargement de Spring DbUnit et Spring Test DbUnit.

III-A-1 - Cas à tester

L'implémentation ci-dessous de « DaoSimple » (présentée dans le chapitre II-B-1. Cas à tester) :

DaoSimple.java

package com.developpez.rpouiller.springtest;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.stereotype.Repository;

@Repository
@Profile(value="implementation")
public class DaoSimple implements IDaoSimple {

private static final String REQUETE_COMPTER =
"SELECT COUNT(*) FROM EXEMPLE WHERE NOM LIKE ?";

@Autowired
private DataSource dataSource;

@Override
public int compter(final String critere) {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
return jdbcTemplate.queryForObject(REQUETE_COMPTER, Integer.class, critere + "%");
}
}

L'annotation @Repository (depuis Spring 2.5) est contenue dans « spring-context ». Elle est également (comme @Service, vue au-dessus) une spécialisation de l'annotation @Component. Elle indique que la classe est une DAO (Data Access Object) et accède à une base de données. L'annotation indique que la classe est éligible par Spring à la translation avec DataAccessException (voir Chapter 10. DAO support).

L'annotation @Profile (depuis Spring 3.1) indique que le bean est activé avec le profil « implementation ». Elle est contenue dans « spring-context ».

La classe JdbcTemplate (depuis Spring 1.0) simplifie l'accès aux données. Dans la version courante de Spring, la méthode queryForObject peut lever les exceptions IncorrectResultSizeDataAccessException et DataAccessException. Cette classe est contenue dans « spring-jdbc ».

La classe NamedParameterJdbcTemplate est une alternative à la classe JdbcTemplate. Elle permet de lire ou modifier des données en passant par des requêtes SQL paramétrées.

DaoSimple.java en remplaçant le JdbcTemplate par un NamedParameterJdbcTemplate

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

private static final String REQUETE_COMPTER =
"SELECT COUNT(*) FROM EXEMPLE WHERE NOM LIKE :nom";

@Override
public int compter(final String critere) {
final NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
final Map<String, Object> params = new HashMap<String, Object>();
params.put("nom", critere + "%");
return jdbcTemplate.queryForObject(REQUETE_COMPTER, params, Integer.class);
}

Il faut donc rajouter la dépendance « spring-jdbc » dans le fichier « pom.xml » :

Extrait du fichier « pom.xml »

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.0.RELEASE</version>
<scope>test</scope>
</dependency>

Avec cette nouvelle dépendance, cela donne l'arbre de dépendances suivant :

Extrait du résultat de la commande « mvn dependency:tree »

[INFO] com.developpez.rpouiller:spring-test-article:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework:spring-context:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] |  +- org.springframework:spring-beans:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-core:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] |  \- org.springframework:spring-expression:jar:4.1.0.RELEASE:compile
[INFO] +- org.springframework:spring-test:jar:4.1.0.RELEASE:test
[INFO] +- junit:junit:jar:4.11:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] +- org.hsqldb:hsqldb:jar:2.3.2:compile
[INFO] \- org.springframework:spring-jdbc:jar:4.1.0.RELEASE:test
[INFO]    \- org.springframework:spring-tx:jar:4.1.0.RELEASE:test

III-A-2 - Test avec Spring DbUnit

Comme indiqué précédemment, la classe de test ci-dessous utilise Spring DbUnit.

DaoSimpleTestAvecSpringDBUnit.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

import com.excilys.ebi.spring.dbunit.test.DataSet;
import com.excilys.ebi.spring.dbunit.test.DataSetTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="DaoSimpleTest-context.xml")
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
DataSetTestExecutionListener.class })
@ActiveProfiles(profiles="implementation")
public class DaoSimpleTestAvecSpringDBUnit {

@Autowired
private IDaoSimple dao;

@Test
@DataSet("donnees.xml")
public void testCompterAvecC() {
// Arrange
final String critere = "C";
final int expected = 5;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}

@Test
@DataSet("donnees.xml")
public void testCompterAvecCh() {
// Arrange
final String critere = "Ch";
final int expected = 2;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}
}

Le listener « DataSetTestExecutionListener » s'occupe de charger les données avant l'exécution de la méthode de test.

L'annotation « @DataSet » permet d'indiquer qu'il faut charger le fichier « donnees.xml » (juste en dessous). Si le fichier n'est pas indiqué, le fichier par défaut est « dataSet.xml ». En l'absence d'information dans l'annotation « @DataSet » précisant le nom de la source de données (avec « dataSourceSpringName ») les données sont chargées grâce au bean Spring implémentant « DataSource » (voir fichier de configuration Spring un peu après). Si le type de base n'est pas précisé avec « dbType », on considère qu'il s'agit d'une base HSQLDB, donc cela nous convient très bien.

L'annotation @ActiveProfiles (depuis Spring 3.1) active le profil « implementation ». En plus simple, dans notre exemple, cela indique à Spring qu'il doit injecter « DaoSimple » pour « IDaoSimple » (voir l'annotation @Profile au chapitre III-A-1).

donnees.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Exemple identifiant="1" nom="Chocolat" />
<Exemple identifiant="2" nom="Chien" />
<Exemple identifiant="3" nom="Framboise" />
<Exemple identifiant="4" nom="Cacao" />
<Exemple identifiant="5" nom="Caramel" />
<Exemple identifiant="6" nom="Cerise" />
<Exemple identifiant="7" nom="Fraise" />
</dataset>

Il est nécessaire d'ajouter la dépendance suivante dans le fichier « pom.xml » :

Extrait des dépendances du fichier « pom.xml »

<!-- Début des dépendances pour Spring-DBUnit -->
<dependency>
<groupId>com.excilys.ebi.spring-dbunit</groupId>
<artifactId>spring-dbunit-test</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<!-- Fin des dépendances pour Spring-DBUnit -->

Comme indiqué dans la page dédiée à Maven du projet, les artefacts n'existent pas sur mvnrepository. Il est donc également nécessaire d'ajouter le dépôt suivant :

Extrait des dépôts du fichier « pom.xml »

<repositories>
<repository>
<id>excilys-release</id>
<url>http://repository.excilys.com/content/repositories/releases</url>
</repository>
</repositories>

Avec la dépendance vers HSQLDB (HyperSQL DataBase), ajoutée auparavant, cela donne l'arbre de dépendances suivant :

Extrait du résultat de la commande « mvn dependency:tree »

[INFO] com.developpez.rpouiller:spring-test-article:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework:spring-context:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] |  +- org.springframework:spring-beans:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-core:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] |  \- org.springframework:spring-expression:jar:4.1.0.RELEASE:compile
[INFO] +- org.springframework:spring-test:jar:4.1.0.RELEASE:test
[INFO] +- junit:junit:jar:4.11:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] +- org.hsqldb:hsqldb:jar:2.3.2:compile
[INFO] +- org.springframework:spring-jdbc:jar:4.1.0.RELEASE:test
[INFO] |  \- org.springframework:spring-tx:jar:4.1.0.RELEASE:test
[INFO] \- com.excilys.ebi.spring-dbunit:spring-dbunit-test:jar:1.4.0:test
[INFO]    \- com.excilys.ebi.spring-dbunit:spring-dbunit-core:jar:1.4.0:test
[INFO]       +- org.springframework:spring-orm:jar:4.0.7.RELEASE:test
[INFO]       +- org.slf4j:slf4j-api:jar:1.7.5:test
[INFO]       +- org.dbunit:dbunit:jar:2.5.0:test
[INFO]       |  \- commons-collections:commons-collections:jar:3.2.1:test
[INFO]       \- org.slf4j:jcl-over-slf4j:jar:1.7.5:test

Le fichier de configuration Spring contient l'activation de la configuration par annotations avec « component-scan » et les paramètres de la source de données qui est utilisée par DbUnit.

DaoSimpleTest-context.xml

<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

<context:component-scan base-package="com.developpez.rpouiller.springtest" />

<bean id="dataSource" class="org.hsqldb.jdbc.JDBCDataSource">
<property name="url" value="jdbc:hsqldb:file:base/exemple" />
<property name="user" value="sa" />
<property name="password" value="" />
</bean>
</beans>

III-A-3 - Test avec Spring Test DbUnit

C'est maintenant la classe de test utilisant Spring Test DbUnit.

DaoSimpleTestAvecSpringTestDBUnit.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="DaoSimpleTest-context.xml")
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
DbUnitTestExecutionListener.class })
@ActiveProfiles(profiles="implementation")
public class DaoSimpleTestAvecSpringTestDBUnit {

@Autowired
private IDaoSimple dao;

@Test
@DatabaseSetup("donnees.xml")
public void testCompterAvecC() {
// Arrange
final String critere = "C";
final int expected = 5;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}

@Test
@DatabaseSetup("donnees.xml")
public void testCompterAvecCh() {
// Arrange
final String critere = "Ch";
final int expected = 2;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}
}

Il est nécessaire d'ajouter les dépendances suivantes dans le fichier « pom.xml » :

Extrait des dépendances du fichier « pom.xml »

<!-- Début des dépendances pour Spring-Test-DBUnit -->
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
<scope>test</scope>
</dependency>
<!-- Fin des dépendances pour Spring-Test-DBUnit -->

Cela donne l'arbre de dépendances suivant :

Extrait du résultat de la commande « mvn dependency:tree »

[INFO] com.developpez.rpouiller:spring-test-article:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework:spring-context:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] |  +- org.springframework:spring-beans:jar:4.1.0.RELEASE:compile
[INFO] |  +- org.springframework:spring-core:jar:4.1.0.RELEASE:compile
[INFO] |  |  \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] |  \- org.springframework:spring-expression:jar:4.1.0.RELEASE:compile
[INFO] +- org.springframework:spring-test:jar:4.1.0.RELEASE:test
[INFO] +- junit:junit:jar:4.11:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] +- org.hsqldb:hsqldb:jar:2.3.2:compile
[INFO] +- org.springframework:spring-jdbc:jar:4.1.0.RELEASE:test
[INFO] |  \- org.springframework:spring-tx:jar:4.1.0.RELEASE:test
[INFO] +- com.excilys.ebi.spring-dbunit:spring-dbunit-test:jar:1.4.0:test
[INFO] |  \- com.excilys.ebi.spring-dbunit:spring-dbunit-core:jar:1.4.0:test
[INFO] |     \- org.slf4j:jcl-over-slf4j:jar:1.7.5:test
[INFO] +- com.github.springtestdbunit:spring-test-dbunit:jar:1.1.0:test
[INFO] +- org.springframework:spring-orm:jar:4.1.0.RELEASE:test
[INFO] +- org.dbunit:dbunit:jar:2.5.0:test
[INFO] |  \- commons-collections:commons-collections:jar:3.2.1:test
[INFO] +- org.slf4j:slf4j-log4j12:jar:1.7.7:test
[INFO] |  \- log4j:log4j:jar:1.2.17:test
[INFO] \- org.slf4j:slf4j-api:jar:1.7.7:test

III-B - Tests avec modifications dans une base de données

Nous allons maintenant voir des tests sur des opérations de modifications. Il sera également nécessaire (comme pour les « Tests avec consultation dans une base de données ») de charger des informations dans la base de données, mais de plus il sera nécessaire de contrôler les informations dans la base. Le test consistera à modifier les informations, et le contrôle de l'état de celles-ci sera déclaré par annotation.

Nous verrons les annotations pour le contrôle de Spring DbUnit et Spring Test DbUnit.

III-B-1 - Cas à tester

Dans l'interface « IDaoSimple », nous ajoutons les nouvelles méthodes « supprimer » qui (assez logiquement) supprime une occurrence dans la base et « creer » qui en crée une.

IDaoSimple.java

package com.developpez.rpouiller.springtest;

public interface IDaoSimple {

int compter(final String critere);
void supprimer(final int identifiant);
void creer(final String nom);
}

Nous ajoutons l'implémentation des méthodes « supprimer » et « creer » dans la classe « DaoSimple ».

DaoSimple.java

private static final String REQUETE_SUPPRIMER =
"DELETE FROM EXEMPLE WHERE IDENTIFIANT = ?";
private static final String REQUETE_CREER =
"INSERT INTO EXEMPLE(IDENTIFIANT, NOM) " +
"VALUES ((SELECT MAX(IDENTIFIANT) + 1 FROM EXEMPLE), ?)";

@Override
public void supprimer(final int identifiant) {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update(REQUETE_SUPPRIMER, identifiant);
}

@Override
public void creer(final String nom) {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update(REQUETE_CREER, nom);
}

Dans la version courante de Spring, la méthode update peut lever des exceptions DataAccessException.

III-B-2 - Test avec Spring DbUnit

Voici l'import et les méthodes à ajouter dans la classe « DaoSimpleTestAvecSpringDBUnit » pour tester ces deux nouvelles méthodes.

DaoSimpleTestAvecSpringDBUnit.java

import com.excilys.ebi.spring.dbunit.test.ExpectedDataSet;

@Test
@DataSet("donnees.xml")
@ExpectedDataSet("donnees-prevues.xml")
public void testSupprimer() {
// Arrange
final int identifiant = 2;

// Act
dao.supprimer(identifiant);
}

@Test
@DataSet("donnees.xml")
@ExpectedDataSet(locations={"donnees-prevues-creer.xml"},
columnsToIgnore={"identifiant"})
public void testCreer() {
// Arrange
final String nom = "Test";

// Act
dao.creer(nom);
}

L'annotation « @ExpectedDataSet » permet d'indiquer les fichiers à utiliser pour contrôler l'état de la base à la fin du test. Nous avons le fichier « donnees-prevues.xml » (juste en dessous) pour contrôler le résultat de la suppression et le fichier « donnees-prevues-creer.xml » (juste en dessous du fichier « donnees-prevues.xml ») pour contrôler la création (« columnsToIgnore » permet de ne pas tenir compte de certaines colonnes lors du contrôle).

Le fichier « donnees-prevues.xml » contient les valeurs après la suppression de l'occurrence avec l'« identifiant » à la valeur « 2 ».

donnees-prevues.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Exemple identifiant="1" nom="Chocolat" />
<Exemple identifiant="3" nom="Framboise" />
<Exemple identifiant="4" nom="Cacao" />
<Exemple identifiant="5" nom="Caramel" />
<Exemple identifiant="6" nom="Cerise" />
<Exemple identifiant="7" nom="Fraise" />
</dataset>

Le fichier « donnees-prevues-creer.xml ». contient les valeurs avec en plus une occurrence avec la colonne « nom » à la valeur « Test ».

donnees-prevues-creer.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Exemple identifiant="1" nom="Chocolat" />
<Exemple identifiant="2" nom="Chien" />
<Exemple identifiant="3" nom="Framboise" />
<Exemple identifiant="4" nom="Cacao" />
<Exemple identifiant="5" nom="Caramel" />
<Exemple identifiant="6" nom="Cerise" />
<Exemple identifiant="7" nom="Fraise" />
<Exemple identifiant="" nom="Test" />
</dataset>

III-B-3 - Test avec Spring Test DbUnit

Voici l'import et les méthodes à ajouter dans la classe « DaoSimpleTestAvecSpringTestDBUnit » pour tester ces deux nouvelles méthodes :

DaoSimpleTestAvecSpringTestDBUnit.java

import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;

@Test
@DatabaseSetup("donnees.xml")
@ExpectedDatabase("donnees-prevues.xml")
public void testSupprimer() {
// Arrange
final int identifiant = 2;

// Act
dao.supprimer(identifiant);
}

@Test
@DatabaseSetup("donnees.xml")
@ExpectedDatabase(assertionMode=DatabaseAssertionMode.NON_STRICT,
value="donnees-prevues-creer2.xml")
public void testCreer() {
// Arrange
final String nom = "Test";

// Act
dao.creer(nom);
}

L'annotation « @ExpectedDatabase » (comme juste avant l'annotation « @ExpectedDataSet ») permet d'indiquer les fichiers à utiliser pour contrôler l'état de la base à la fin du test. Nous avons le fichier « donnees-prevues.xml » pour contrôler le résultat de la suppression et le fichier « donnees-prevues-creer2.xml » (juste en dessous) pour contrôler la création, (« assertionMode=DatabaseAssertionMode.NON_STRICT » permet d'indiquer de ne pas tenir compte des colonnes absentes du fichier « donnees-prevues-creer2.xml », lors du contrôle).

Le fichier « donnees-prevues-creer2.xml » contient les valeurs et une occurrence avec la colonne « nom » à la valeur « Test ». On peut remarquer que la colonne « identifiant » est absente des valeurs de la table « Exemple ».

donnees-prevues-creer2.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<Exemple nom="Chocolat" />
<Exemple nom="Chien" />
<Exemple nom="Framboise" />
<Exemple nom="Cacao" />
<Exemple nom="Caramel" />
<Exemple nom="Cerise" />
<Exemple nom="Fraise" />
<Exemple nom="Test" />
</dataset>

III-C - Comparaison de Spring DbUnit et Spring Test DbUnit

Spring DbUnit

Spring Test DbUnit

Page du projet

https://github.com/excilys/spring-dbunit

https://github.com/springtestdbunit/spring-test-dbunit

Version actuelle

1.4.0 (1 octobre 2014)

1.1.0 (29 janvier 2014)

Compatible Spring 4

oui

oui

Chargement de données en base par annotation

oui, avec DataSet

oui, avec DatabaseSetup

Contrôle de données en base par annotation

oui, avec ExpectedDataSet

oui, avec ExpectedDatabase

Customisation du chargement des données depuis un fichier

oui, avec attribut format de DataSet et ExpectedDataSet (mais liste limitée aux possibilités de DataSetFormat)

oui, avec dataSetLoader ou dataSetLoaderBean de DbUnitConfiguration (pas de limite, la classe ou le bean est une implémentation de DataSetLoader)

Connexion à la base de données

avec un bean DataSource (défini avec l'attribut dataSourceSpringName de DataSet

avec un bean DataSource ou un bean IDatabaseConnection (défini avec l'attribut databaseConnection de DbUnitConfiguration

Version compatible Spring 3

1.1.12 (4 décembre 2012)

1.0.1 (20 janvier 2013)

Présent sur le dépôt mvnrepository

non

oui

Le projet « Spring Test DbUnit » comporte quelques fonctionnalités (plus secondaires) en plus que le projet « Spring DbUnit ». Le premier a également, au moment de la rédaction de l'article, plus de « Star » et de « Fork ». Par contre, « Spring DbUnit » a été compatible Spring 4.0 avant « Spring Test DbUnit ».

III-D - Autres solutions (alternatives)

À part ces deux projets qui permettent de déclarer les données par annotations, il est possible d'utiliser des solutions plus classiques : par exemple avec DbUnit en direct ou avec le récent DbSetup (voir l'article Introduction à DbSetup, une alternative à DbUnit).

Dans l'exemple ci-dessous de tests de consultation dans une base de données avec DbSetup, on peut voir que DbSetup permet de charger des données par l'intermédiaire d'une Rule :

DaoSimpleTestAvecDbSetup.java

package com.developpez.rpouiller.springtest;

import static org.junit.Assert.assertEquals;

import javax.sql.DataSource;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.DbSetupTracker;
import com.ninja_squad.dbsetup.Operations;
import com.ninja_squad.dbsetup.destination.DataSourceDestination;
import com.ninja_squad.dbsetup.operation.Operation;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="DaoSimpleTest-context.xml")
@ActiveProfiles(profiles="implementation")
public class DaoSimpleTestAvecDbSetup {

private static DbSetupTracker dbSetupTracker = new DbSetupTracker();

@Autowired
private IDaoSimple dao;

@Autowired
@Qualifier(value="dataSource")
private DataSource dataSource;

@Rule
public ExternalResource externalResource = new ExternalResource() {

@Override
protected void before() throws Throwable {
final Operation operation =
Operations.sequenceOf(
Operations.deleteAllFrom("EXEMPLE"),
Operations.insertInto("EXEMPLE")
.columns("identifiant", "nom")
.values("1", "Chocolat")
.values("2", "Chien")
.values("3", "Framboise")
.values("4", "Cacao")
.values("5", "Caramel")
.values("6", "Cerise")
.values("7", "Fraise")
.build());

final DbSetup dbSetup = new DbSetup(new DataSourceDestination(dataSource), operation);
dbSetupTracker.launchIfNecessary(dbSetup);
}
};

@Test
public void testCompterAvecC() {
dbSetupTracker.skipNextLaunch();

// Arrange
final String critere = "C";
final int expected = 5;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}

@Test
public void testCompterAvecCh() {
dbSetupTracker.skipNextLaunch();

// Arrange
final String critere = "Ch";
final int expected = 2;

// Act
final int valeur = dao.compter(critere);

// Assert
assertEquals(expected, valeur);
}
}

Il est nécessaire d'ajouter la dépendance suivante dans le fichier « pom.xml » :

Extrait des dépendances du fichier « pom.xml »

<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>DbSetup</artifactId>
<version>1.3.0</version>
</dependency>

IV - Conclusion

Mis à part des tests unitaires sans base de données avec Spring, nous avons pu avoir un aperçu de deux solutions permettant de réaliser des tests avec une base de données.

Chacun de ces projets comporte quelques autres fonctionnalités intéressantes, non présentées dans cet article, que vous pourrez trouver dans la documentation et/ou dans les sources des projets.

Je vous invite à aller voir ces deux projets Open Source et à ne pas hésiter à participer, si vous en avez l'envie. Les deux projets sont hébergés sur GitHub avec toute la souplesse de Git, vous permettant de réaliser un « fork » avec votre propre correction/évolution, puis de proposer votre travail grâce au « pull request ».

Personnellement, pour les besoins de cet article, j'ai apporté une petite contribution au projet Spring DbUnit. J'en profite pour remercier Stéphane Landelle pour son accueil et sa relecture technique.

Les sources de cet article sont présentes sur GitHub : https://github.com/regis1512/spring-test-article.

V - Remerciements

Je remercie très sincèrement :

- www.developpez.com qui me permet de publier cet article ;

- Nono40 et djibril pour leurs outils ;

- Thierry Leriche-Dessirier et Logan Mauzaize pour leur relecture technique ;

- milkoseck et Jacques THÉRY pour leur relecture orthographique rigoureuse.