Tests JUnit4 combiné avec Spring et Spring MVC en mode transactionnel

Le titre de ce billet montre bien l'étendue des thèmes variés qui seront traités. Il a pour objectif d'illustrer, à l'aide d'un exemple assez complet et proche des cas réels, la mise en place des tests, en mode transactionnel, pour les différentes couches applicatives. Ainsi les vraies difficultés rencontrées par les développeurs seront évoquées.

Le billet traite 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.

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, ...

La pratique des tests unitaires est l'un des principes des méthodes agiles. Il semble que tous les nouveaux frameworks renforcent le principe de testabilité de toutes les couches applicatives.

Avec JUnit4 et Spring, les tests, en particulier d'intégration, deviennent aisés. Ceux ayant pratiqués les tests savent bien les efforts nécessaires afin de tester certaines couches( par exemple, la couche de persistance ou DAO).

Citons au passage, qu' un critère majeur permettant de juger de la suffisance des tests peut être :

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.

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!

La figure suivante résume les objectifs des différents types de tests pour un projet. tests-synthese

La bonne compréhension de ce billet nécessite d'avoir certains pré-requis énoncés plus loin.
A défaut, au préalable, lire article sur le blog Netapsys .
Par contre, l'utilisation de Spring avec JUnit4 n'exige en aucun cas de maîtriser Spring.

Dans l'exemple détaillé plus loin, Spring 2.5 allège considérablement la configuration XML.
De plus, les principes "ZERO CONFIGURATION" et/ou "CONVENTION OVER CONFIGURATION" permettent d'alléger plus les fichiers de configuration xml.

La présentation de ce billet est divisée en trois grandes parties:

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) :

[xml]
<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]
<?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>

En dehors des commentaires et explications, ce fichier contient peu de lignes. Et même si le projet continue à grossir, ce fichier de configuration n'évolue que très peu.

Soulignons que l’emploi des namespaces (par exemple mlns:context) réduit énormément la verbosité de la configuration XML de Spring.

La dernière ligne définit la vue retournée en réponse à une requête http.

Remarque importante:
Dans le fichier web.xml déjà présenté, la variable «contextConfigLocation» pointe, entre autres, vers le fichier "spring.xml".
Ceci afin que le contexte de l’application charge aussi les beans nécessaires.
Attention, en l'absence de cette indication, vous seriez en face d’exceptions difficiles à déchiffrer !

La classe du "controller de SpringMVC" est nommée "ClientControllerSpringMVC.java".
Le controller 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 Spring mVC "ClientControllerSpringMVC.java" est comme suit:

[java]
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 de Spring2.5 va scanner, via <context:component-scan> du fichier spring-mvc-webapp-servlet.xml, les beans dans les packages mentionnés.

Noter également la présence d'autres stéréotypes @Repository ou @Service qui sont commenté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ée ci-après.

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]
<?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 :

[java]
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 :

[java]
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);
	}

	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;
	}

	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;
		}
	}

	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:

[java]
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.IDao;
import com.netapsys.fr.springmvc.exceptions.MyException;
@Service()
public class MyService implements IService{
	private static final long serialVersionUID = 1L;

	private IDao daoImpl;
	@Autowired
	public void setDaoImpl(IDao 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 apsects transverses tels que les logs et les mesures de temps d'exécution.

ETAPE 5. Fichiers jsp

Le fichier index.jsp ne fait que rediriger 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".
Le "controller" décide ensuite de la vue à rendre en réponse à cette requête.
Dans notre cas, c'est la méthode "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, nous lançons une console dos puis, dans le répertoire du projet, 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 ci-contre capture-indexjsp .

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 :

[java]
/*-- 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("
\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) de Spring.

Spring offre aussi des annotations inexistantes dans JUnit4 utiles à l'exécution de ces tests.

Signalons que la ligne:

[java]
@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:

[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 :

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

Pour tester lancer la commande "mvn test".
Les commentaires du code et les traces de log4j 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.

L'apport de Spring au framework de test JUnit est important. Juste quelques annotations 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.

ETAPE 10. Annexes (pom et codes sources)

La figure ci-après illustre l'arborescence du projet
arbo_projetJunit4SpringMVC.

Le fichier zip contenant le projet ainsi que les détails du pom.xml sont joints à ce billet.

5 commentaires

  1. je suis débutant en spring j’ai donc suivi le tuto du bout en bout qui est bien fait, puis j’ai fait un copier coller de votre projet dans le répertoire créé pour cet effet au moment de la compile j’ai une erreur que voici :

    org.apache.maven.surefire.booter.SurefireExecutionException: org/junit/Assume$AssumptionViolatedException; nested exception is java.lang.NoClassDefFoundError: org/junit/Assume$AssumptionViolatedException
    java.lang.NoClassDefFoundError: org/junit/Assume$AssumptionViolatedException
    at org.springframework.test.context.junit4.SpringMethodRoadie.runTestMethod(SpringMethodRoadie.java:240)
    at org.springframework.test.context.junit4.SpringMethodRoadie$RunBeforesThenTestThenAfters.run(SpringMethodRoadie.java:333)
    at org.springframework.test.context.junit4.SpringMethodRoadie.runWithRepetitions(SpringMethodRoadie.java:217)
    at org.springframework.test.context.junit4.SpringMethodRoadie.runTest(SpringMethodRoadie.java:197)
    at org.springframework.test.context.junit4.SpringMethodRoadie.run(SpringMethodRoadie.java:143)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.invokeTestMethod(SpringJUnit4ClassRunner.java:142)
    at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:59)
    at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:52)
    at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:34)
    at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:44)
    at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:50)
    at org.apache.maven.surefire.junit4.JUnit4TestSet.execute(JUnit4TestSet.java:62)
    at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:140)
    at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:127)
    at org.apache.maven.surefire.Surefire.run(Surefire.java:177)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:338)
    at org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:997)
    Caused by: java.lang.ClassNotFoundException: org.junit.Assume$AssumptionViolatedException
    at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
    at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
    … 21 more
    14:23:06,515 INFO (AbstractApplicationContext.java:816) Closing org.springframework.context.support.GenericApplicationContext@152c4d9: display name [org.springframework.context.support.GenericApplicationContext@152c4d9]; startup date [Sat Nov 21 14:23:05 CET 2009]; root of context hierarchy
    14:23:06,515 INFO (DefaultSingletonBeanRegistry.java:399) Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@adb1d4: defining beans [clientControllerSpring,org.springframework.context.annotation.internalPersistenceAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,daoImpl,myService,dataSource,transactionManager]; root of factory hierarchy
    [ERROR]

    Mojo:

    org.apache.maven.plugins:maven-surefire-plugin:2.4.2:test

    FAILED for project:

    com.netapsys.springmvc:spring-mvc-webapp:war:1.0-SNAPSHOT

    Reason:

    There are test failures.

    Please refer to C:\\ECLIPSE_3.5\\JUnitSpringSpringMVC_ModeTrasactionel\\spring-mvc-webapp\\target\\surefire-reports for the individual test results.

    [INFO] ————————————————————————
    [INFO] For more information, run with the -e flag
    [INFO] ————————————————————————
    [INFO] BUILD FAILED
    [INFO] ————————————————————————
    [INFO] Total time: 6 seconds
    [INFO] Finished at: Sat Nov 21 14:23:06 CET 2009
    [INFO] Final Memory: 10M/24M
    [INFO] ————————————————————————

    Cordialement merzouk.

  2. l’erreur vient de la version de JUnit il faut la 4.4 pas la 4.5 car la classe Assume$AssumptionViolatedException n’existe pas dans la version 4.5, et d’autre erreurs sont apparues par la suite.

  3. J’avais fait le tutoriel https://www.sodifrance.fr/blog/index.php/p

    J’avais hate d’essayer cet article que je considère comme la suite.

    J’ai été déçu :
    L’article n’est pas construit de façon itérative. Si on copie-colle la premier classe elle ne compile même pas à cause de toutes les dépendences.

    Ensuite je suis étonné d’avoir dans MyService une référence à DAOImpl plutôt qu’à IDao. Pourquoi dépendre d’une implémentation ?

  4. Ensuite je suis étonné d’avoir dans MyService une référence à DAOImpl plutôt qu’à IDao. Pourquoi dépendre d’une implémentation ?

    @intercrew,

    Merci, c’est corrigé dans l’article.

  5. Woah! I’m really enjoying the template/theme of this site.
    It’s simple, yet effective. A lot of times it’s very difficult to get that « perfect balance » between user friendliness and appearance.
    I must say you’ve done a awesome job with this. Also, the blog loads vsry fast for
    me on Safari. Excellent Blog!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.