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 :
Sur les tests unitaires 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 :
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 :
package
com.developpez.rpouiller.springtest;
public
interface
ICasSimple {
int
additionner
(
final
int
param1, final
int
param2);
}
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).
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring-context</artifactId>
<version>
4.1.0.RELEASE</version>
</dependency>
Cela donne l'arbre de dépendances suivant :
[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.
<?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 ».
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 :
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 » :
<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 :
[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 :
package
com.developpez.rpouiller.springtest;
public
interface
IServiceSimple {
int
compter
(
final
String critere);
}
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.
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.
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.
<?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.
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. TestTest, 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 :
Il faut donc rajouter la dépendance Maven suivante :
<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 :
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 à testerCas à tester) :
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.
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 » :
<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 :
[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.
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-1Cas à tester).
<?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 » :
<!-- 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 :
<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 :
[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.
<?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.
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 » :
<!-- 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 :
[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éesTests 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.
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 ».
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.
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 ».
<?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 ».
<?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 :
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 ».
<?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 |
||
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 |
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 :
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 » :
<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.