Recevez les mises à jour gratuites du blog par Email : »» Garanti sans spam indésirable ««

JUnit : Test unitaire hors conteneur J2EE avec Spring et JNDI

de Mimie le 19 janvier 2010

Rubrique : Programmation

Principe

Lors du développement d’une application web (en Java ou pas) il est conseillé de créer des classes de tests afin de vérifier premièrement que le code écrit correspondant bien aux spécifications fonctionnelles demandées, et deuxièmement que la fonctionnalité (ou un ensemble de fonctionnalités) reste valide lorsque des demandes d’évolution sont intégrées à l’application.

Ces classes de tests nous permettent donc rapidement de savoir que le code à livrer est cohérent et correspondant aux spécifications fonctionnelles.

La bibliothèque JUnit est faite pour réaliser ce genre de tests dans un environnement Java, la version utilisée dans l’exemple est la 4.8.1.

(il est cependant rare que le développeur ait le temps d’écrire une classe de test pour chaque fonctionnalité qu’il code, par manque de temps principalement)

Besoins

Nous devons effectuer des tests unitaires de base de données en utilisant des services métier définis dans des fichiers de configurations Spring. Ces services métier accèdent à la base de données via une variable JNDI (Java Naming and Directory Interface) déclaré à la fois au sein de notre conteneur J2EE (serveur d’applications) et dans notre application de la façon suivante :

<!-- ###### JNDI Lookup ###### -->
<bean id="dbGeeks" class="org.springframework.jndi.JndiObjectFactoryBean" lazy-init="true">
	<property name="jndiName" value="jdbc/dbGeeks" />
</bean>

Comment lancer nos tests en mode batch (hors conteneur J2EE) utilisant notre fichier de contexte Spring déclaré ci-dessus sachant qu’il utilise un contexte JNDI propre au serveur d’application ?

Tel est l’enjeu de cet article, deux solutions s’offrent à nous :

  1. La première consiste à utiliser l’attribut defaultObject de la classe JndiObjectFactoryBean qui permet de changer de source de données lorsque l’appel JNDI échoue
  2. La seconde consiste à créer une classe qui va créer pour nous le contexte JNDI avant d’utiliser nos services dans les fichiers de configuration Spring

Base de données

  • Les tests réalisés dans l’exemple permettent simplement de se connecter à une base de données et de récupérer le contenu d’une table ‘user’ afin de vérifier que l’accès en base se fait correctement. La base de données utilisée est MySQL version 5.1.41.
  • Script de création de la table
  • CREATE TABLE IF NOT EXISTS `user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `login` varchar(50) NOT NULL,
      `pass` varchar(200) NOT NULL,
      `address` varchar(500) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `login` (`login`)
    ) ENGINE=InnoDB;
    

Solution 1

La première solution consiste donc à passer vers une source de données de substitution lorsque l’appel JNDI échoue, tout se passe au niveau de la configuration Spring. Les paramètres de la source de données « local » prend donc des paramètres en dur ou par le biais d’un fichier de properties :

  • fichier de configuration Spring
<!-- ###### JNDI Lookup ###### -->
<bean id="dbGeeks" class="org.springframework.jndi.JndiObjectFactoryBean" lazy-init="true">
	<property name="jndiName" value="jdbc/dbGeeks" />
	<!--  fallback to a local datasource if we are not in the container -->
	<property name="defaultObject" ref="localDataSource" />
</bean>
<bean id="localDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="com.mysql.jdbc.Driver" />
	<property name="url" value="jdbc:mysql://localhost:3306/des_geeks?autoReconnect=true" />
	<property name="username" value="root" />
	<property name="password" value="" />
</bean>
  • classe de test JUnit permettant la récupération du service Spring « userService » et son utilisation
public class TestUserServiceCase1 extends TestCase {
	UserService userService;
	@Override
	protected void setUp() throws Exception {
		super.setUp();
		String[] springFiles = { "applicationContext-metier-case1.xml" };
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext(springFiles);
		userService = (UserService) applicationContext.getBean("userService");
	}
	public void testGetAllUsers() throws Exception {
		List<UserBean> usersList = userService.getAllUsers();
		System.out.println(usersList);
	}
}

Solution 2

La seconde solution consiste à référencer le contexte JNDI avant l’instanciation des objets Spring, ce qui permet de ne pas toucher du tout aux fichiers de configurations Spring, une classe Java fait l’affaire :

  • fichier de configuration Spring d’origine
<!-- ###### JNDI Lookup ###### -->
<bean id="dbGeeks" class="org.springframework.jndi.JndiObjectFactoryBean" lazy-init="true">
	<property name="jndiName" value="jdbc/dbGeeks" />
</bean>
  • classe Java permettant d’initialiser le contexte JNDI
public class JndiDatasourceCreator {
	/** constants datasource */
	private static final String url = "jdbc:mysql://localhost:3306/des_geeks?autoReconnect=true";
	private static final String username = "xxxx";
	private static final String password = "yyyy";
	private static final String jndiName = "dbGeeks";
	public static void create() throws Exception {
		try {
			// initialisation du contexte
			System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
			InitialContext context = new InitialContext();
			// création d'une référence sur la DataSource
			Reference reference = new Reference("javax.sql.DataSource", "org.apache.commons.dbcp.BasicDataSourceFactory", null);
			reference.add(new StringRefAddr("driverClassName", "com.mysql.jdbc.Driver"));
			reference.add(new StringRefAddr("url", url));
			reference.add(new StringRefAddr("username", username));
			reference.add(new StringRefAddr("password", password));
			// liaison de la DataSource au contexte
			context.rebind("jdbc/" + jndiName, reference);
		} catch (NamingException ex) {
			ex.printStackTrace();
		}
	}
}
  • classe de test JUnit permettant la récupération du service Spring « userService » et son utilisation
public class TestUserServiceCase2 extends TestCase {
	UserService userService;
	@Override
	protected void setUp() throws Exception {
		super.setUp();
		JndiDatasourceCreator.create();
		String[] springFiles = { "applicationContext-metier-case2.xml" };
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext(springFiles);
		userService = (UserService) applicationContext.getBean("userService");
	}
	public void testGetAllUsers() throws Exception {
		List<UserBean> usersList = userService.getAllUsers();
		System.out.println(usersList);
	}
}

Conclusion

C’est important de pouvoir lancer ces test unitaires ou d’intégration en se détachant complètement du serveur d’applications. La mise en place d’une de ces méthodes permet donc de faire cela tranquillement sans avoir à bouleverser votre code (surtout la seconde solution).

Sources disponibles ici

Cet article a été écrit par :

– qui a déjà rédigé 123 posts sur Des Geeks et des lettres.

Passionné d'informatique et développeur JavaEE de métier, je me consacre principalement à écrire des billets sur les sujets du Web et de la programmation Web. Ce blog est un espace qui me permet de partager mes découvertes avec vous et me sert accessoirement de pense bête !

Contacter l'auteur

Jetez aussi un oeil sur :

{ 11 commentaires… à vous de vous exprimer ! }

1 Greg janvier 19, 2010 à 21 h 26 min

Avec tout ça c’est quand que tu nous ponds un super site ?!

Répondre

2 Mimie janvier 19, 2010 à 22 h 42 min

J’y travaille :-) et ce sera un site sur magic the gathering ^^

Répondre

3 Mimie janvier 21, 2010 à 8 h 03 min

Article mis à jour, j’avais oublié de vous présenter la classe Java permettant de réaliser l’initialisation du contexte JNDI dans la seconde solution.
Néanmoins tout se trouve dans le source, ++

Répondre

4 Rodrigo Hjort février 8, 2010 à 2 h 35 min

Super ! Mais j’ai eu l’impression qu’il fallait mieux introduire la classe « com.sun.jndi.fscontext.RefFSContextFactory » utilisée dans la deuxième solution.

Répondre

5 Mimie février 8, 2010 à 9 h 55 min

Bonjour Rodrigo, comme toi la seconde solution me parait plus correcte en considérant la première solution plus comme une astuce qu’autre chose :-) néanmoins les deux solutions sont viables c’est pourquoi j’ai voulu les présenter dans cet article, merci à toi d’être passé, au plaisir.

Répondre

6 omar janvier 20, 2011 à 17 h 47 min

Voici une autre solution que j’utilise sur un projet :
Je configure le jndi dans un fichier disons « datasourceContext.xml » sans oublier de lui donner un alias.
Exemple :

java:comp/env/jdbc/AppDB

Dans les tests unitaire j’importe un autre fichier xml: dataSourceTest.xml à la palce du précédent

Concernant la seconde solution proposée, elle a l’inconvénient d’internaliser la configuration dans du code java

java:comp/env/jdbc/appDB

Pour mes tests unitaires, je ne charge pas ce fichier mais un autre datasourceTest.xml

Répondre

7 Mimie janvier 20, 2011 à 23 h 18 min

@omar: ton argument sur la deuxième solution ne tient pas étant donné que la classe Java peut faire appel à un fichier de propriétés (.properties) externe qui sera pour le coup équivalent à un fichier de configuration .xml de Spring.
De plus je n’ai pas très bien saisi ta solution personnelle, le fait d’avoir 2 fichiers distinct change quoi réellement ? quel est le contenu de ces deux fichiers ?

Répondre

8 omar janvier 21, 2011 à 21 h 02 min

Tu dis « la classe Java peut faire appel à un fichier de propriétés (.properties) externe qui sera pour le coup équivalent à un fichier de configuration .xml de Spring. »

Or mon commentaire faisait référence uniquement à la classe JndiDatasourceCreator
qui peut etre pourrait utiliser un properties mais ce n’est pas le cas dans l’exemple où les paramètres sont codés en static.

Mon jndi est défini dans un fichier datasourceContext.xml :
[xml]
<bean id="tomcatDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="resourceRef" value="true"/>
<property name="jndiName">
<value>java:comp/env/jdbc/ScribeDB</value>
</property>
</bean>
<alias alias="dataSource" name="tomcatDataSource"/>
[/xml]

Et durant les tests unitaires, j’importe en lieu et place de ce
dernier un dataSourceTest.xml au contenu suivant :
[xml]
<!–
Création de la dataSource
ici une connexion jdbc vers la base données
–>
<bean id="testDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.postgresql.Driver"/>
<property name="url" value="%{dataSource.url}"/>
<property name="username" value="%{dataSource.username}"/>
<property name="password" value="%{dataSource.password}"/>
</bean>
<alias alias="dataSource" name="testDataSource"/>
[/xml]

Voila.

Répondre

9 Mimie janvier 21, 2011 à 21 h 10 min

@omar: pour afficher du code il suffit d’utiliser les balises suivantes :
[plain]
[xml] … [/xml], [java] … [/java], etc.
[/plain]
- Ton premier point est juste, seulement je pensais que tu n’appréciais pas la seconde méthode parce que justement il fallait mettre en dur les paramètres, je voulais juste souligner qu’un .properties pouvait palier à ce problème.
- Pour ta séparation des fichiers de config Spring je comprends mieux ce que tu fais, cependant à quoi te sert ton alias « dataSource » ? comment l’exploites-tu dans ton appel Java ? peux-tu nous laisser un exemple de ton code Java ?

Merci d’être repassé pour les précisions :)

Répondre

10 omar janvier 22, 2011 à 12 h 02 min

Salut,
Je n’a pas précisé ou était injecté le dataSource car l’article lui-même ne le précisait pas comment
dbGeeks était injecté dans le UserService.

Sinon l’alias dataSource est injecté ainsi :
[xml]
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean" depends-on="dataSource">
<description>
Factory hibernate qui va nous permettre de récupérer des sessions.
On lui passe la "underlying jdbc datasource" (lien jdbc avec la base)
ainsi que le gestionnaire de transactions (pas besoin de JTA ici
puisqu’on a une seule source de donnée, la base postgres).
</description>
<property name="dataSource" ref="dataSource"/>
<property name="configLocation">
<value>classpath:hibernate.cfg.xml</value>
</property>
</bean>
[/xml]

Enfin la sessionFactory est injecté dans le constructeur un kebabDao qui lui-même est utilisé dans un kebabService.

Une autre variante plus simple est au lieu d’importer des fichiers de configuration différents (ici un dataSourceTest en test ), c’est d’importer le fichier utilisé par le serveur & de redéfinir le bean du même nom.

Ceci est possible en important un autre fichier
qui possède un bean du même nom. Et le dernier qui est trouvé par Spring est retenu.

Exemple: un test unitaire importe :
– appplicationContext.xml qui lui importe tout (dont un bean appelé monSuperDataSource)
- plus un dataSourceTest.xml qui définit un bean appelé monSuperDataSource. Etant le dernier importé du meme nom, c’est lui qui sera injecté par Spring.

C’est aussi utile pour importer par redéfinition des beans mockés qui n’attaquent pas un ldap, si externe etc. mais retourne des valeurs quelconques car hors scope du test.

Répondre

11 Mimie janvier 22, 2011 à 13 h 14 min

Merci omar, je vois que nous faisons la même chose, c’est rassurant.

Répondre

Laissez un Commentaire

Article précédent:

Article suivant: