Tests transactionnels JUnit4 combiné avec Spring et SpringMVC

Par :
abderrazek
lun, 11/05/2009 - 21:49
Niveau :
Facile

Ce billet a pour objectif d'illustrer, à l'aide d'un exemple assez complet et proche des cas réels, la mise en place des tests transactionnels pour l'ensemble des couches applicatives. Ainsi les vraies difficultés rencontrées par les développeurs seront évoquées.
Il aborde JUnit4 enrichi avec les annotations de Spring 2.5+ et ses lanceurs pour exécuter facilement les tests. Des illustrations en mode transactionnel vous sont proposées à la fin de ce billet.
Le framework JUnit est l'oeuvre conjointe de Kent Beck (créateur de XP) et Erich Gamma (auteur des Design Patterns).
Avec la version 4, JUnit tente de rattraper son retard sur Testng tout en gardant la compatibilité avec JUnit3x ainsi qu'une parfaite intégration aux éditeurs Eclipse, Netbeans, ...
Avec les lanceurs de spring, les tests deviennent plus attrayants. Spring encourage ainsi à adopter l'approche TDD "Test Driven Design" ou "Test-First Developpment".
Notez que le jdk5+ est nécessaire pour certaines parties de code Java. Les commentaires dans le code java le mentionnent au bon endroit.
La pratique des tests unitaires est l'un des principes des méthodes agiles.
Avec JUnit4 et Spring, les tests, en particulier d'intégration, deviennent aisés. 
L'un des avantages des tests est d'avoir un retour (feedback) rapide et beaucoup moins cher sur les réglages à apporter au logiciel et ainsi d'anticiper les anomalies.
Le second avantage des tests (unitaires et d'intégration) est de limiter le nombre d'itérations (en phase recette/production) de mise en conformité du logiciel.
Et, par conséquent, de réduire son coût total. Signalons qu'en phase de mise en recette/production les personnes impliquées sont de diverses compétences d'où le coût économique élevé d'une itération à ce stade!
Avant de rentre dans le vif su sujet, rappelons qu'un critère permettant de mesurer la suffisance des tests :
"L'investissement fait en tests doit être égal à celui passé sur le design. Et si le design répond facilement au changement alors les tests sont suffisants."

PREREQUIS

Les pré-requis suivants aideront à lire facilement ce billet. Mais tous ne sont pas nécessaires à sa compréhension.

  1. Connaissance des applications Web dans le monde JEE (servlet, jsp..) ;
  2. Connaissance sommaire de Spring, Spring MVC avec ses annotations ;
  3. Connaissance sommaire de JUnit4.x avec ses annotations ;
  4. Connaissance sommaire de la notion de transaction.

ETAPE 1. Application Web avec Spring MVC

Pour toute la suite, l’application Web exemple sera nommée «spring-mvc-webapp». C’est le nom de la servlet frontale dans le fichier web.xml.
Commençons par configurer le fichier web.xml dont voici le contenu (certaines lignes peuvent encore être simplifiées mais sont laissées ici pour une meilleure compréhension) :

<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring-mvc-webapp-servlet.xml, classpath:/spring.xml
</param-value>
</context-param>
 
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- déclare la servlet frontal -->
<servlet>
<servlet-name>spring-mvc-webapp</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- les requêtes se terminant par .html sont servies par cette servlet -->
<servlet-mapping>
<servlet-name>spring-mvc-webapp</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
</web-app>


Notons que l'arborescence du projet sera donnée dans l'annexe de ce billet.
La partie 'contextConfigLocation' sera explicitée plus loin : on reviendra en détail sur les fichiers spring*.xml.
Le «listener» permet de configurer le contexte du Spring MVC.
Le bloc "<servlet>...</servlet>" permet d'identifier la servlet frontale de Spring MVC chargée de répondre à toutes les requêtes (*.html) d'un client de l’application Web.
La section suivante détaille le fichier «spring-mvc-webapp-servlet.xml», nommé ainsi conformément à la convention.

ETAPE 2. Configuration de Spring MVC

Le fichier «spring-mvc-webapp-servlet.xml» doit contenir ces lignes :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Fichier de conf du contexte d'application pour Spring (fichier nommé spring-mvc-webapp-servlet.xml selon la convention. -->
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"

default-autowire="byName">

<!-- - Tous les controlleurs sont automatiquement détectés grâce à l'annotation @Controller.
- On définit ici dans quel package le post processor doit chercher ces beans annotés. -->

 
<context:component-scan base-package="com.netapsys.fr.springmvc.web"/>
 
<!-- Activates various annotations to be detected in bean classes: Spring's
@Required and @Autowired, as well as JSR 250's @PostConstruct,@PreDestroy and
@Resource (if available) and JPA's @PersistenceContext & @PersistenceUnit.
-->

<context:annotation-config/>
<!--
- Les controlleurs de cette application fournissent une annotation @RequestMapping
- Ils peuvent être déclarés de deux manière différentes:
- Au niveau de la classe :
- par exemple @RequestMapping("/addVisit.html")
- Pour ce type de controlleurs on peut annoter les méthodes pour une requete Post ou Get,
- Au niveau de chaque méthode. Différents exemples seront fournis.
-->

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
<!--
Ceci est le view resolver, il permet de définir la technologie de vue utilisée et comment
sélectionner une vue. On prendra ici la solution la plus simple : elle permet de mapper
le nom de la vue retournée avec la sélection d'une jsp. Ex. : si le nom de la vue retournée est "hello" alors on utilisera le fichier
WEB-INF/jsp/hello.jsp pour construire la vue.
-->

<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>

</beans>

A noter bien la présence au début de ce fichier de l'instruction <<default-autowire="byName">>. Celle-ci, systématiquement utilisée dans tous les fichiers de configuration de Spring, afin d'auto-injecter les beans de Spring en utilisant pluôt leur noms et non leurs types (valeur par défaut). 
Depuis la version 2.5, Spring a étendu l'annotation @Resource en introduisant l'annotation @Autowired pour augmenter plus le niveau de contrôle de l'auto-injection. L'annotation @Autowired devient active dés que le bean AutowiredAnnotationBeanPostProcessor est défini dans le fichier xml ou encore plus simplement dés que le namespace 'context' est introduit avec l'instruction <context:annotation-config/> comme illustré ci-dessus.
Signalons que l’emploi des namespaces (par exemple mlns:context) réduit énormément la verbosité de la configuration XML de Spring.
En dehors des commentaires et explications, ce fichier contient peu de lignes. Si le projet continue à grossir, ce fichier de configuration n'évolue que très peu.
La dernière ligne, très bien commentée, définit la vue retournée en réponse à une requête http.
Remarque importante: dans le fichier web.xml ci-dessus, la variable «contextConfigLocation» pointe, entre autres, vers le fichier "spring.xml" afin que le contexte de l’application Web charge aussi d’autres beans nécessaires. 
Attention, en l'absence de cette indication, vous seriez en face d’exceptions difficiles à déchiffrer ! 

Ci-après, la classe controller de SpringMVC se charge de traiter les requêtes transmises par la servlet dispatcher de SpringMVC en réponse aux requêtes http du client de l'application web.

Controller de spring MVC

Le code de la classe multi-controller SpringMVC nommée ClientControllerSpringMVC.java est comme suit:

package com.netapsys.fr.springmvc.web;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
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 org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.SessionStatus;
import com.netapsys.fr.springmcv.entites.Client;
import com.netapsys.fr.springmvc.exceptions.MyException;
import com.netapsys.fr.springmvc.service.MyService;
import com.netapsys.fr.springmvc.tb.constants.Constants;
 
@Controller("clientControllerSpring")
public class ClientControllerSpringMVC {
private MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
final Logger logger=Logger.getLogger(getClass().getName());
/**
* Handler de la méthode Get pour l'URL /getClientSpringMVC.html.
* @param nom le nom du theme affiché dans la vue.
* @param prenom
* @param model une map de toutes les données qui seront utilisables dans la vue
* @return le <code>Constants.SUCCESS or Constants.ECHEC</code> nom de la vue qu'il faudra utiliser.
*/

@RequestMapping(value="/getClient.html",method = RequestMethod.GET)
public String getClient(@RequestParam(value=Constants.ATTRIBUTE_NAME,required=true) String nom,
@RequestParam(value=Constants.ATTRIBUTE_LASTNAME,required=false) String prenom,ModelMap model) {
Client client=myService.getClient(nom, prenom);
if(client!=null ) {
logger.info(">>Client '"+prenom+" "+nom+"' existe.");
model.addAttribute("client",client);
return Constants.SUCCESS;
}else {
model.addAttribute("errorMsg", "Client '"+prenom+" "+nom+"' inexistant");
return Constants.ECHEC;
}
}
@RequestMapping(value="/createClient.html",method = RequestMethod.GET)
public String createClient( @RequestParam(value=Constants.ATTRIBUTE_NAME,required=true) String nom,
@RequestParam(value=Constants.ATTRIBUTE_LASTNAME,required=false) String prenom, ModelMap model) {
Client client=null;
try {
client=myService.createClient( nom, prenom);
} catch (MyException e) {e.printStackTrace();}
model.addAttribute("client",client);
logger.info("client created="+client);
return Constants.SUCCESS;
}
@RequestMapping(value="/updateClient.html",method = RequestMethod.POST)
public String updateClient( @ModelAttribute("client") Client client,BindingResult result, SessionStatus status) {
logger.info("Client to update "+client.toString());
myService.updateClient( client.getCliId(), client.getCliNom(),client.getCliPrenom());
status.setComplete();
logger.info(">>>Client update OK");
return null;
}
}

Notez que la classe est annotée avec le stéréotype @Controller. Ainsi ses méthodes vont être analysées par la servlet dispatcher pour traiter toutes les requêtes(*.html). L'auto-détection (injection) de Spring2.5 va scanner, dans les packages indiqués par <context:component-scan> du fichier spring-mvc-webapp-servlet.xml, tous les beans.
D'autres stéréotypes @repository ou @Service sont expliqués dans le code java.
Mises à part les annotations @RequestMapping et @RequestParam, le "controller" ne fait qu'appeler les méthodes de la couche service détaillé à l'étape 4 ci-après.
Avant cela, l'étape 3 qui suit revient sur le contenu du fichier spring.xml.

ETAPE 3. Configuration Spring des beans des couches DAO et Service

Le fichier de configuration Spring nommé spring.xml (pas de convention ici !) sert à déclarer les beans métier qui seront consommés par l’application Web. Il indique les packages java à scanner pour l'auto-injection de ces beans. Enfin, il déclare une dataSource pour la couche DAO.

<?xml version="1.0" encoding="UTF-8"?>
<!-- spring.xml -->
<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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/tx/spring-aop-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"

__default-autowire__="byName">

<!--
Activates various annotations to be detected in bean classes: Spring's
@Required and @Autowired, as well as JSR 250's @PostConstruct,
@PreDestroy and @Resource (if available) and JPA's @PersistenceContext
and @PersistenceUnit (if available).
-->

<context:annotation-config/>
 
<context:component-scan base-package="com.netapsys.fr.springmvc.dao"/>
<context:component-scan base-package="com.netapsys.fr.springmvc.service"/>
 
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource">

<property name="driverClassName"
value="com.mysql.jdbc.Driver">

</property>
<property name="url"
value="jdbc:mysql://localhost:3306/test">

</property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>

ETAPE 4. Classes DAO et Service

- Couche DAO :

Notez que cette couche utilise deux classes beans d'entités ( Client.java et Personne.java). Leurs codes, simples, sont donnés en annexe.

Le code de l'interface IDao.java contient :

package com.netapsys.fr.springmvc.dao;
import com.netapsys.fr.springmcv.entites.Client;
import com.netapsys.fr.springmvc.exceptions.MyException;
public interface IDao {
boolean isExistId(String id);
boolean findByName(String nom);
Client getClient(String nom);
Client getClient(String nom,String prenom);
Client getClient(long id);
Client createClient(String nom, String prenom) throws MyException;
Client updateClient(long id,String nom, String prenom);
void deleteClient(long id);
}

Et l'implémentation de cette interface est faite dans la classe DaoImpl.java qui contient les lignes suivantes :

package com.netapsys.fr.springmvc.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Repository;
import com.netapsys.fr.springmcv.entites.Client;
import com.netapsys.fr.springmvc.tb.constants.Constants;
@Repository /** @Repository un marqueur Spring pour, entre autres, auto-translater les exception de la couche de persistance.**/
public class DaoImpl implements IDao{
final private Logger logger = Logger.getLogger(getClass().getName());
/**
* spring template for jdbc (jdk 5ou+)
*/

private SimpleJdbcTemplate jt=null;
@SuppressWarnings("unused")
private DataSource dataSource;
@Autowired
public void setDataSource(DataSource dataSource) {
//Recommandation de spring: Initialiser SimpleJdbcTemplate ici avec new lorsqu'une seule datasource est nécessaire!
jt=new SimpleJdbcTemplate(dataSource);
}
/* (non-Javadoc)
* @see com.netapsys.springmvc.metier.IService#getClient(java.lang.String)
*/

public Client getClient(String nom) {
return getClient(nom,null);
}
public Client getClient(long id) {
return getClient( String.valueOf(id) );
}
public Client getClient(String nom, String prenom) {
String sql=Constants.SQL_REQUETE_CLIENT + " WHERE upper(CLINOM)='"+nom.toUpperCase()+"'";
if(prenom!=null && !"".equals(prenom))
sql+=" AND upper(CLIPRENOM)='"+prenom.toUpperCase()+"'";
if(!findByName(nom) ) {
return (Client)null; //Si nom n existe pas dans bd renvoie null;
}
ParameterizedRowMapper<Client> mapper=new ParameterizedRowMapper<Client>(){
public Client mapRow(ResultSet rs,int rowNm) throws SQLException{
return populateClient(rs);
}
};
Client client=jt.queryForObject(sql, mapper);
logger.info("get Client = "+client);
return client;
}
private Client populateClient(final ResultSet rs) throws SQLException {
if(rs==null) return null;
Client client=new Client();
client.setCliId ( rs.getLong("cliId") );
client.setNom ( rs.getString("cliNom") );
client.setPrenom ( rs.getString("cliPrenom"));
return client;
}
/* (non-Javadoc)
* @see com.netapsys.springmvc.metier.IDao#createClient(long, java.lang.String, java.lang.String)
*/

public Client createClient( String nom, String prenom) {
Client client=new Client();
client.setNom(nom);
client.setPrenom(prenom);
try{
jt.update( Constants.SQL_REQUETE_INSERT_CLIENT+ "'"+nom+ "' , '"+prenom+"'" +")" );
long id=jt.queryForLong("select LAST_INSERT_ID()"); //Mysql retrieve the last id inserted
client.setCliId(id);
return client;
}catch(DataAccessException e){
logger.error( e.getMessage());
return null;
}
}
/*
* @see com.netapsys.springmvc.metier.IService#exist(java.lang.String)
*/

public boolean isExistId(String id) {
final String sql = Constants.SQL_REQUETE_COUNT_CLIENT+" WHERE cliId='" +id + "'";
int count = jt.queryForInt(sql);
return count > 0 ? true : false;
}
public boolean findByName(String nom) {
return findByName(nom,null);
}
public boolean findByName(final String nom,final String prenom) {
 
String sql = Constants.SQL_REQUETE_COUNT_CLIENT+
" WHERE UPPER(cliNom)='" +nom.toUpperCase() + "'";
if(prenom!=null && !"".equals(prenom))
sql+=" AND UPPER(CLIPRENOM)='"+prenom.toUpperCase() + "'";
int count = jt.queryForInt(sql);
return count > 0 ? true : false;
}
public void deleteClient(long id) {
final String sql=Constants.SQL_DELETE_ALL_CLIENT+" WHERE cliId='"+ id + "'";
jt.update(sql);
}
public Client updateClient(long id, String nom, String prenom) {
Client client=new Client();
client.setCliId(id);
client.setCliNom(nom);
client.setCliPrenom(prenom);
final String sql="update Client set clinom='"+
nom+"', cliprenom='"+prenom+"' WHERE cliId='"+id+"'";
logger.info("nb rows updated="+jt.update(sql) );
return client;
}

,

- Couche service :

Enfin, la couche service contient, en dehors de son interface identique à celle de IDao, la classe d'implémentation nommée MyService.java dont voici son code:

package com.netapsys.fr.springmvc.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.netapsys.fr.springmcv.entites.Client;
import com.netapsys.fr.springmvc.dao.DaoImpl;
import com.netapsys.fr.springmvc.exceptions.MyException;
@Service()
public class MyService implements IService{
private static final long serialVersionUID = 1L;
/** * @see com.netapsys.fr.springmvc.dao.DaoImpl */
private DaoImpl daoImpl;
@Autowired
public void setDaoImpl(DaoImpl daoImpl) {
this.daoImpl = daoImpl;
}
public Client getClient(String nom, String prenom) {
return daoImpl.getClient(nom, prenom);
}
public boolean isExistId(String id) {
// Valider les regles de gestion .....if id is not int/long...
return daoImpl.isExistId(id);
}
public boolean findByName(String nom) {
return daoImpl.findByName(nom);
}
public Client getClient(String nom) {
return daoImpl.getClient(nom);
}
public Client createClient( String nom, String prenom) throws MyException {
return daoImpl.createClient( nom, prenom);
}
public void deleteClient(long id) {
daoImpl.deleteClient(id);
}
public Client getClient(long id) {
return daoImpl.getClient(id);
}
public Client updateClient(long id, String nom, String prenom) {
return daoImpl.updateClient(id, nom, prenom);
}
}



La couche service est là pour appeler les méthodes de la couche DAO. Et c'est dans la couche service que l'on gère l'aspect transactionnel mais également les règles métier spécifiques. On peut faire de même pour tous les aspects transverses tels que les logs, les mesures de temps d'exécution...

ETAPE 5. Fichiers jsp

Le fichier index.jsp redirige la requête vers l'url /createClient.html.

La requête /createClient.html est interceptée par la servlet frontale de SpringMVC, qui à son tour décide de l'action (méthode) à appeler dans le "controller" nommé "ClientControllerSpringMVC" et qui décide ensuite de la vue à rendre en réponse à cette requête. Dans notre cas, c'est la méthode "public String createClient" de la classe "ClientControllerSpringMVC.java" qui sera appelée. Celle-ci, en cas de succès renvoie la chaîne "success" stockée dans Constants.SUCCESS de la classe utilitaire Constants. C'est cette chaîne qui permet de traduire la vue gérant la présentation de la réponse, dans notre cas "success.jsp".

Voici donc les quelques lignes de index.jsp:

[jsp]
<html>
<head>
<title>Spring mvc sample</title>
</head>
<body>
<%
final String urlAction="/createClient.html?";
final String nom = request.getParameter("name");
final String prenom = request.getParameter("lastName");
if (nom != null && !"".equals(nom))
response.sendRedirect(request.getContextPath()
+ urlAction+"name=" + nom + "&lastName"
+ prenom);
else
response
.sendRedirect(request.getContextPath()
+ urlAction+"name=nom007&lastName=prenom007");
%>
</body>
</html>


Le fichier success.jsp:

[jsp]
<body>
<h2>Edition de la fiche client créé </h2><br />
<% final String url=request.getContextPath()+"/updateClient.html";%>
<form:form commandName="client" action="<%=url %>" method="post">
<table>
<tr><td></td>
<td><form:hidden path="cliId" /></td>
</tr>
<tr><td>Nom:</td>
<td><form:input tabindex="1" autocomplete="true" path="cliNom" /></td>
</tr>
<tr><td>Prénom:</td>
<td><form:input tabindex="2" autocomplete="true" path="cliPrenom" /></td>
</tr>
<tr<td colspan="2">
<input type="submit" value="Valider" />
</td>
</tr>
</table>
</form:form>
</body>
</html>

 

Enfin, la source du fichier error.jsp:

[jsp]
<body>
<% String msgError=(String)request.getAttribute("errorMsg"); %>
<br /><h2> </h2><br /><b><%=msgError%></b>
</body>
</html>



Dans success.jsp, l'action du tag "<form:from" pointe sur /updateClient.html avec la méthode http "POST". La méthode "public String updateClient" du "controller" renvoie constamment "null". Ainsi, la vue utilisée dans ce cas est /WEB-INF/jsp/updateClient.jsp conformément aux déclarations du fichier spring-mvc-webapp-servlet.xml.

Le fichier updateClient.jsp contient ces lignes:

[jsp]
<body>
<h2>Page Modification client</h2><br/>
<%
final String path=request.getContextPath();
final String urlAction=path+"/updateClient.html";
%>
<form:form commandName="client" action="<%=urlAction %>" method="post">
<table border=0>
<tr>
<td colspan="2"><form:hidden path="cliId" id="id"/></td>
</tr>
<tr>
<td>Nom:</td>
<td><form:input autocomplete="true" path="cliNom" /></td>
</tr>
<tr>
<td>Prénom:</td>
<td><form:input autocomplete="true" path="cliPrenom" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Valider" />
</td>
</tr>
</table>
</form:form>
</body>
</html>

ETAPE 6. Tests de l'application web

Pour tester l'application web ainsi complétée, nous allons lancer une console dos, puis se positionner dans le répertoire du projet et lancer la commande :

[sh]
mvn jetty:run


S'assurer que la base Mysql nommée test (contenant une table client avec trois champs cliId, cliNom et cliPrenom) est en service.
Puis lancer le navigateur web avec l'url http://localhost:8080/spring-mvc-webapp/index.jsp.

Vous devez obtenir la figure (Fig2) ci-contre 

ETAPE 7. Classe de test du controlleur SpringMVC

La classe JUnit4, nommée "ClientControllerSpringMVCTest.java", permet de tester le "controller" de SpringMVC. Elle est constituée des lignes suivantes :

/*-- Attention, les puristes de Junit ne verront pas les assert ici!!!!!! */
package com.netapsys.tests.springmvc.web.tests;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
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.ui.ModelMap;
import com.netapsys.fr.springmvc.tb.constants.Constants;
import com.netapsys.fr.springmvc.web.ClientControllerSpringMVC;
@ContextConfiguration(locations={"classpath:/config/spring-mvc-webapp-tests.xml","classpath:/config/spring-test.xml"})
@RunWith( SpringJUnit4ClassRunner.class) //to activate autowiring injection dependence
public class ClientControllerSpringMVCTest {
final static Logger logger =Logger.getLogger(ClientControllerSpringMVCTest.class.getClass().getName());
/**
* Attribute in url, ex. "/getClient.html?name=Agent007
*/

final String NAME2TEST="Agent007";
final String LASTNAME2TEST="007";
private static ModelMap model;
protected ClientControllerSpringMVC clientControllerSpring;
/**
* auto dependency injection par spring du controller dans les tests
*/

@Autowired
public void setClientControllerSpring(ClientControllerSpringMVC clientControllerSpring) {
this.clientControllerSpring = clientControllerSpring;
}
@BeforeClass() public static void testAvantTout(){ model = new ModelMap(); }
@AfterClass() public static void apresTousLesTests(){ model.clear(); }
@Before public void initAvant(){
if(clientControllerSpring!=null) /** testons que spring injection est ok*/
logger.info("\n\tclientControllerSpring is correctly initialized!");
}
@After public void testApres(){}
@Test
public void testGetClient() {
//create or get client with given nom. if not exist create it with nom & prenom
final String str=clientControllerSpring.getClient(NAME2TEST, LASTNAME2TEST, model);
if((Constants.SUCCESS).equals(str)) logger.info( model.get("client") );
}
}


Les parties importantes pour la compréhension des lanceurs de spring (annotés par @RunWith) sont bien documentées dans le code.
L'annotation @RunWith définit le lanceur spring qui enrichit considérablement les tests JUnit4 avec les fonctionnalités supplémentaires ( ex. l'auto-injection) offertes par Spring.
Spring offre aussi des annotations inexistantes dans JUnit4 utiles à l'exécution de ces tests.
Signalons que la ligne:

@ContextConfiguration(locations={"classpath:/config/spring-mvc-webapp-tests.xml","classpath:/config/spring-test.xml"})


renvoie à deux fichiers de configuration de springMVC et de spring qui sont donnés en annexe. Ces deux fichiers sont identiques, à une déclaration près, aux fichiers spring-mvc-webapp-servlet.xml et spring.xml explicités auparavant. La seule différence est l'ajout de la déclaration de la transaction dans spring-test.xml:

<!-- transaction -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>


Ainsi, avec le bean "transactionManager" les tests en mode transactionnel ci-après deviennent possibles comme illustré à l'étape suivante.

ETAPE 8.Classe de test en mode transactionnel

Les deux figures suivantes donnent le code de la classe MyServiceTest.java. Cette classe comporte toutes les indications pour excéuter les méthodes en mode transactionnel :

</p><p><span style="font-weight: bold;">package</span> com.<span style="color: #006600;">netapsys</span>.<span style="color: #006600;">tests</span>.<span style="color: #006600;">springmvc</span>.<span style="color: #006600;">web</span>.<span style="color: #006600;">tests</span><span style="color: #66cc66;">;</span><br /><span style="color: #a1a100;">import java.util.Random;</span><br /><span style="color: #a1a100;">import org.apache.log4j.Logger;</span><br /><span style="color: #a1a100;">import org.junit.*;</span><br /><span style="color: #a1a100;">import org.junit.runner.RunWith;</span><br /><span style="color: #a1a100;">import org.springframework.beans.factory.annotation.Autowired;</span><br /><span style="color: #a1a100;">import org.springframework.test.context.ContextConfiguration;</span><br /><span style="color: #a1a100;">import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;</span><br /><span style="color: #a1a100;">import org.springframework.test.context.transaction.AfterTransaction;</span><br /><span style="color: #a1a100;">import org.springframework.test.context.transaction.BeforeTransaction;</span><br /><span style="color: #a1a100;">import org.springframework.test.context.transaction.TransactionConfiguration;</span><br /><span style="color: #a1a100;">import org.springframework.transaction.annotation.Propagation;</span><br /><span style="color: #a1a100;">import org.springframework.transaction.annotation.Transactional;</span><br /><span style="color: #a1a100;">import com.netapsys.fr.springmcv.entites.Client;</span><br /><span style="color: #a1a100;">import com.netapsys.fr.springmcv.entites.Personne;</span><br /><span style="color: #a1a100;">import com.netapsys.fr.springmvc.exceptions.MyException;</span><br /><span style="color: #a1a100;">import com.netapsys.fr.springmvc.service.MyService;</span><br /><span style="font-style: italic; color: #808080;">/** * *******************************************************************<br /> * NOTE IMPORTANTE: defaultRollback à true ANNULE TOUTES LES ACTIONS DANS LA BASE!<br /> * ******************************************************************** */</span><br />@ContextConfiguration<span style="color: #66cc66;">(</span>locations=<span style="color: #66cc66;">{</span><span style="color: #ff0000;">"classpath:/config/spring-mvc-webapp-tests.xml"</span>,<span style="color: #ff0000;">"classpath:/config/spring-test.xml"</span><span style="color: #66cc66;">}</span><span style="color: #66cc66;">)</span><br />@RunWith<span style="color: #66cc66;">(</span> value=SpringJUnit4ClassRunner.<span style="font-weight: bold;">class</span> <span style="color: #66cc66;">)</span> <span style="font-style: italic; color: #808080;">//indispensable</span><br />@TransactionConfiguration<span style="color: #66cc66;">(</span>transactionManager=<span style="color: #ff0000;">"transactionManager"</span>,defaultRollback=<span style="font-weight: bold;">true</span><span style="color: #66cc66;">)</span><br /><span style="font-weight: bold;">public</span> <span style="font-weight: bold;">class</span> MyServiceTest <span style="color: #66cc66;">{</span><br /> <span style="font-weight: bold;">private</span> <span style="font-weight: bold;">final</span> Logger logger = Logger.<span style="color: #006600;">getLogger</span><span style="color: #66cc66;">(</span>getClass<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span>.<span style="color: #006600;">getName</span><span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="font-style: italic; color: #808080;">/** * AUTO INJECTION PAR SPRING */</span><br /> <span style="font-weight: bold;">private</span> MyService myService<span style="color: #66cc66;">;</span><br /> @Autowired <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> setMyService<span style="color: #66cc66;">(</span>MyService myService<span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span><br /> <span style="font-weight: bold;">this</span>.<span style="color: #006600;">myService</span> = myService<span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><br /> @BeforeClass <span style="font-weight: bold;">public</span> <span style="font-weight: bold;">static</span> <span style="color: #993333;">void</span> beforeClasse<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span><span style="color: #66cc66;">}</span><br /> @Before<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span> <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> avantChaqueTest<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span><br /> <span style="color: #b1b100;">if</span><span style="color: #66cc66;">(</span>myService==<span style="font-weight: bold;">null</span><span style="color: #66cc66;">)</span> logger.<span style="color: #006600;">error</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"Y a vraiment un probleme avec spring"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><span style="font-style: italic; color: #808080;">//verifie que l'auto injection de spring est ok</span><br /> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;Before test...(this is in transaction scope)"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span> <br /> <span style="color: #66cc66;">}</span><br /> @After<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span> <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> apresChaqueTest<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;After test...(this is in transaction scope)"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span> <span style="color: #66cc66;">}</span><br /> @BeforeTransaction<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span> <br /> <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> avantTransaction<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;Avant chaque transaction"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><span style="color: #66cc66;">}</span><br /> @AfterTransaction <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> apresTransac<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;Apres chaque transaction"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><span style="color: #66cc66;">}</span><br /> <span style="font-style: italic; color: #808080;">/** * PREMIER TEST ****/</span><br /> @Test @Transactional <span style="color: #66cc66;">(</span>propagation=Propagation.<span style="color: #006600;">REQUIRED</span><span style="color: #66cc66;">)</span><br /> <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> testCreateClient<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span><br /> <span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">String</span> nom =giveRandomName<span style="color: #66cc66;">(</span><span style="color: #ff0000;">"Agent007test"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">String</span> prenom=giveRandomName<span style="color: #66cc66;">(</span><span style="color: #ff0000;">"007test"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> Client client=<span style="font-weight: bold;">null</span><span style="color: #66cc66;">;</span><br /> <span style="font-weight: bold;">try</span><span style="color: #66cc66;">{</span><br /> client=myService.<span style="color: #006600;">getClient</span><span style="color: #66cc66;">(</span>nom,prenom<span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #b1b100;">if</span><span style="color: #66cc66;">(</span>client<span style="color: #66cc66;">!</span>=<span style="font-weight: bold;">null</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span><br /> logger.<span style="color: #006600;">warn</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"client "</span>+client +<span style="color: #ff0000;">" existe donc ne sera pas créé"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="font-weight: bold;">throw</span> <span style="font-weight: bold;">new</span> MyException<span style="color: #66cc66;">(</span><span style="color: #ff0000;">"Client existe!"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><br /> client=myService.<span style="color: #006600;">createClient</span><span style="color: #66cc66;">(</span> nom, prenom<span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #b1b100;">if</span><span style="color: #66cc66;">(</span>client<span style="color: #66cc66;">!</span>=<span style="font-weight: bold;">null</span><span style="color: #66cc66;">)</span>logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"<span style="color: #000099; font-weight: bold;">\n</span>&gt;&gt;&gt;Client :'"</span>+client.<span style="color: #006600;">toString</span><span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span>+<span style="color: #ff0000;">"' is created!"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><span style="font-weight: bold;">catch</span><span style="color: #66cc66;">(</span>MyException se<span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span><br /> logger.<span style="color: #006600;">error</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;Client exist!<span style="color: #000099; font-weight: bold;">\t</span>"</span>+se.<span style="color: #006600;">getMessage</span><span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><br /> <span style="color: #66cc66;">}</span><br /> <span style="font-style: italic; color: #808080;">/** * 2eme test : */</span><br /> @Ignore @Transactional<span style="color: #66cc66;">(</span>readOnly=<span style="font-weight: bold;">true</span><span style="color: #66cc66;">)</span> <br /> <span style="font-weight: bold;">public</span> <span style="color: #993333;">void</span> testGetClient<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">{</span><br /> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"<span style="color: #000099; font-weight: bold;">\n</span>&gt;&gt;&gt;3eme test getClient."</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">String</span> NAME=<span style="color: #ff0000;">"nom007nnn"</span><span style="color: #66cc66;">;</span><br /> <span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">String</span> LASTNAME=<span style="color: #ff0000;">"prenom007nnn"</span><span style="color: #66cc66;">;</span><br /> Personne client=myService.<span style="color: #006600;">getClient</span><span style="color: #66cc66;">(</span>NAME,LASTNAME<span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #b1b100;">if</span><span style="color: #66cc66;">(</span>client<span style="color: #66cc66;">!</span>=<span style="font-weight: bold;">null</span><span style="color: #66cc66;">)</span><br /> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"&gt;&gt;&gt;&gt;"</span>+client.<span style="color: #006600;">toString</span><span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #b1b100;">else</span> <br /> logger.<span style="color: #006600;">info</span><span style="color: #66cc66;">(</span><span style="color: #ff0000;">"Client '"</span>+NAME+<span style="color: #ff0000;">"' not exist!"</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><br /> <span style="font-weight: bold;">private</span> <span style="color: #aaaadd; font-weight: bold;">String</span> giveRandomName<span style="color: #66cc66;">(</span><span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">String</span> prefix<span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span><br /> <span style="font-weight: bold;">final</span> <span style="color: #aaaadd; font-weight: bold;">Random</span> random=<span style="font-weight: bold;">new</span> <span style="color: #aaaadd; font-weight: bold;">Random</span><span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span> <br /> <span style="font-weight: bold;">return</span> prefix+random.<span style="color: #006600;">nextInt</span><span style="color: #66cc66;">(</span><span style="color: #cc66cc;">100</span><span style="color: #66cc66;">)</span><span style="color: #66cc66;">;</span><br /> <span style="color: #66cc66;">}</span><br /><span style="color: #66cc66;">}</span></p><p>


Pour tester ces différentes classes, lancer la commande "mvn test".
Les commentaires du code et les traces de log4j à retrouver après l'exécution sont très explicites. Ils permettent de voir qu'en positionnant le "defaultRollback" de l'annotation spring "@TransactionConfiguration" à true, ces tests simulent la création d'un client dans la base.
C'est à dire, qu'un rollback est fait à chaque test. On peut modifier le paramètre "defaultRollback=false", et les transactions insèrent bien des clients dans la base.
Mais généralement, en mode test, le defaultRollback est positionné à true.
L'annotation de Spring @Autowired gère fort bien l'auto-injection si on observe la règle suivante : Annoter, avec les @Autowired, toujours les méthodes setters/getters et positionner dans les fichiers de configuration de spring le defaut-autowired à byName afin d'anticiper sur des exceptions indéchiffrables.
C'est là que l'on prend conscience de l'apport de Spring au framework de test JUnit. Juste quelques annotations en plus et les tests deviennent transactionnels.
Les retours bénéfiques de ces tests sont considérables.

ETAPE 9. Conclusion

Combiner Spring 2.5+ et JUnit4 permet d'avoir sous la main un framework de test puissant facilitant la mise en place des tests unitaires et d'intégration. Bien que l'apprentissage exige un léger effort, une fois ces deux frameworks maîtrisés, l'efficacité et le gain économique sont énormes.
Enfin, la qualité du livrable au client ne sera que meilleure.
Observons, en particulier, dans les classes de tests en mode transactionnel, le confort qu'apporte Spring à JUnit4.
Les annotations Spring @TransactionConfiguration et @Transactional rendent les transactions à la portée de tout le monde.
Dans un prochain billet, je reviendrai sur les tests paramétrés dans Spring.

La source en .zip de ce billet est ici.

Ajouter un commentaire

Filtered HTML

Plain text

CAPTCHA
Cette question permet de vérifier que vous n'êtes pas un robot spammeur :-)
  CCC  DDD       J      J  ZZZZZ 
C D D J J Z
C D D J J Z
C D D J J J J Z
CCC DDD JJJ JJJ ZZZZZ