Tags

, , , , ,

Dans ce billet, je vais mettre en œuvre un serveur Jetty Embedded sur une application Spring, afin d’obtenir les fondations d’un système de tests automatisés et semi-automatisés.

En effet, l’objectif, en ce qui me concerne, est de faciliter la recette d’applications via les deux méthodes suivantes :

  1. Tests automatisés pour vérifier la conformité aux spécifications,
  2. Automatisation de la phase d’initialisation des données pour réaliser la partie “manuelle” de la recette. Cela permet de dérouler de multiples scénarios de recette basés sur des pré-requis différents en termes de données, sans avoir à réaliser cette initialisation de façon “artisanale”, généralement laborieuse.

Spring apporte, entre autres, deux avantages appréciables :

  1. Les applications Spring peuvent s’exécuter dans un simple container Web, tel que Jetty ou Tomcat,
  2. L’outillage Spring fournit des outils de mise en route rapide tels que Spring Fuse et Spring ROO.

Je vais m’appuyer sur le tutoriel Beginning With Roo, qui consiste à créer une application de gestion pour une pizzeria, nommée Pizza Shop. L’application permet de confectionner des pizzas et de gérer des commandes.

L’objectif est de mettre en place les fondations pour les deux types de tests décrits ci-dessus. En l’occurrence la partie initialisation des données consistera à créer de façon automatique des bases de pizza (tomate, crème fraîche), et des ingrédients. Les tests, qu’ils soient automatiques ou manuels, pourront donc s’appuyer sur ces données, pour vérifier par exemple qu’une pizza doit posséder une et une seule base, être composée d’un nombre d’ingrédients compris entre un minimum et un maximum, avoir un prix, etc…

pizzashopfinal

Objectif: automatiser l'initialisation des données de l'application

Ceux qui ont déjà réalisé ce tutoriel auront noté qu’à l’étape 6, “Loading the Web Server”, on déploie l’application sur un Tomcat ou un Jetty embedded, via l’une de ces commandes maven :

  • mvn tomcat:run
  • mvn jetty:run

ou bien dans un container configuré dans la vue “server” d’Eclipse / STS.

La méthode que je propose ici permet de faire l’équivalent de façon programmatique, et d’ajouter la phase d’initialisation des données, ouvrant ainsi la porte à l’automatisation des tests.

C’est parti, avec la version 1.2.2 de Spring ROO…

1/ Bâtir l’application Pizza Shop

Le script Spring ROO suivant reprend les commandes du tutoriel de façon à obtenir une ” pizza shop ” opérationnelle :

project --topLevelPackage com.springsource.roo.pizzashop --projectName Pizza --java 6 --packaging JAR
jpa setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY
entity jpa --class ~.domain.Topping
field string --fieldName name --notNull --sizeMin 2
entity jpa --class ~.domain.Base
field string --fieldName name --notNull --sizeMin 2
entity jpa --class ~.domain.Pizza
field string --fieldName name --notNull --sizeMin 2
field number --fieldName price --type java.lang.Float
field set --fieldName toppings --type ~.domain.Topping
field reference --fieldName base --type ~.domain.Base
entity jpa --class ~.domain.PizzaOrder
field string --fieldName name --notNull --sizeMin 2
field string --fieldName address --sizeMax 30
field number --fieldName total --type java.lang.Float
field date --fieldName deliveryDate --type java.util.Date
field set --fieldName pizzas --type ~.domain.Pizza
web mvc setup
web mvc all --package ~.web

Note 1 : personnellement, j’utilise STS. La première ligne de commande est donc remplacée par l’utilisation du wizard ” New > Spring ROO Project ” :

Wizard STS de création de projet Spring ROO

Wizard STS de création de projet Spring ROO

Note 2 : plutôt que de taper les commandes une à une, il est possible de les copier dans un fichier, puis d’exécuter le script entier:

script –file “chemin absolu du fichier script”

Il pourra être utile d’activer également tout ou partie des logs via l’une des commandes ROO suivantes (ou en modifiant directement le fichier log4j.properties pour ceux qui ne souhaitent pas abuser des commandes ROO):

logging setup –level DEBUG
logging setup –level DEBUG –package PROJECT

A partir de là, l’application est opérationnelle. Il est donc possible d’utiliser l’une des méthodes décrites à l’étape 6 du tutoriel pour la voir à l’œuvre, mais bien sûr, sans aucune donnée.

Sous STS, on créera les dossiers src/test/java et src/test/resources, puis, via un clic droit sur le projet, on lancera un ” Maven > Update Project… ” pour que tout soit en ordre.

2/ Configurer Jetty Embedded

Le script Spring ROO suivant va ajouter Jetty Embedded au classpath de test de l’application :

dependency add --groupId org.eclipse.jetty --artifactId jetty-server --version 7.2.2.v20101205 --scope TEST
dependency add --groupId org.eclipse.jetty --artifactId jetty-webapp --version 7.2.2.v20101205 --scope PROVIDED
dependency add --groupId org.eclipse.jetty --artifactId jetty-jsp-2.1 --version 7.2.2.v20101205 --scope  PROVIDED
dependency add --groupId org.mortbay.jetty --artifactId jsp-2.1-glassfish --version 2.1.v20100127 --scope PROVIDED
dependency remove --groupId javax.el --artifactId el-api --version 1.0
dependency remove --groupId javax.servlet --artifactId servlet-api --version 2.5
dependency remove --groupId javax.servlet.jsp --artifactId jsp-api --version 2.1

Ce script remplace notamment les dépendances javax.* indiquées par les version Jetty correspondantes. Il faudra également ajouter les exclusions suivantes au POM du projet pour la même raison :

<dependency>
    <groupId>javax.servlet.jsp.jstl</groupId>
    <artifactId>jstl-api</artifactId>
    <version>1.2</version>
    <exclusions>
        <exclusion>
            <artifactId>servlet-api</artifactId>
            <groupId>javax.servlet</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jsp-api</artifactId>
            <groupId>javax.servlet.jsp</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>jstl-impl</artifactId>
    <version>1.2</version>
    <exclusions>
        <exclusion>
            <artifactId>servlet-api</artifactId>
            <groupId>javax.servlet</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jsp-api</artifactId>
            <groupId>javax.servlet.jsp</groupId>
        </exclusion>
    </exclusions>
</dependency>

3/ Désactiver tilesConfigurer, incompatible avec JUnit

Spring MVC utilise Tiles, mais ce dernier a besoin d’un ServletContext pour fonctionner, non disponible dans un test JUnit.
Une solution pour y remédier (je suis preneur de toute autre) est de supprimer le bean tilesConfigurer du contexte Spring de test.
Ici, nous dupliquerons le fichier webmvc-config.xml qui se trouve dans src/main/webapp/WEB-INF/spring. Nous nommerons cette copie webmvc-config-test.xml et nous y commenterons le bean tilesConfigurer :

<!-- bean class="org.springframework.web.servlet.view.tiles2.TilesConfigurer" id="tilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/layouts/layouts.xml</value>
            <value>/WEB-INF/views/**/views.xml</value>
        </list>
    </property>
</bean -->

Sans cette modification, nous obtiendrions l’erreur suivante à l’exécution du test :

java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:157)
[...]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tilesConfigurer' defined in URL [file:src/main/webapp/WEB-INF/spring/webmvc-config.xml]: Invocation of init method failed; nested exception is java.lang.NullPointerException
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1455)
[...]

4/ Ecrire le test, 1ère étape : démarrage de Jetty et déploiement de l’application

La classe suivante est à placer dans le dossier src/test/java, dans un package com.springsource.roo.pizzashop :

package com.springsource.roo.pizzashop;

import org.apache.log4j.Logger;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/applicationContext*.xml", "file:src/main/webapp/WEB-INF/spring/webmvc-config-test.xml"})
@Transactional
public class Test {

private static final Logger logger = Logger.getLogger(Test.class);
private static int port = 8090;
private static String context = "pizzashop";
private static Server server;
private static String baseUrl;

public static void startServer() {
    // start up server
    server = new Server(port);
    // define deployment
    server.setHandler(new WebAppContext("src/main/webapp", "/" + context));
    try {
        logger.debug("startServer() before start");
        server.start();
    } catch (Exception e) {
        throw new RuntimeException("Probleme demarrage serveur", e);
    }
    // getLocalPort returns the port that was actually assigned
    int actualPort = server.getConnectors()[0].getLocalPort();
    baseUrl = "http://localhost:" + actualPort + "/" + context;
    logger.debug("startServer() serverStarted. baseUrl:" + baseUrl);
}

public static void stopServer() throws Exception {
    if (server != null)
        server.stop();
}

@org.junit.Test
public void test() throws Exception {
    startServer();
    stopServer();
}

}

La méthode startServer() démarre Jetty et déploie l’application à la volée. En plaçant un point d’arrêt sur le stopServer() de la méthode test(), on obtient le même comportement qu’avec les trois options proposées à l’étape 6 du tutoriel Spring ROO.

5/ Ecrire le test, 2ème étape : ajouter l’initialisation des données

Les annotations suivantes permettent aux tests de fonctionner dans le contexte Spring, et d’avoir ainsi accès à la couche de persistance définie pour l’application :

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/META-INF/spring/applicationContext*.xml", "file:src/main/webapp/WEB-INF/spring/webmvc-config-test.xml" })

Il est alors possible d’instancier puis de “persister” les objets du domaine métier, pour réaliser l’initialisation des données. C’est ce que fait la méthode initData() ci-dessous :

@org.junit.Test
public void test() throws Exception {
    startServer();
    initData();
    stopServer();
}

public void initData() throws Exception {
    newBase("Tomate");
    newBase("Creme fraiche");
    newTopping("Emmental");
    newTopping("Mozzarella");
    newTopping("Chorizo");
    newTopping("Jambon");
    newTopping("Saumon");
    newTopping("Anchois");
    newTopping("Fruits de mer");
    newTopping("Champignons");
    newTopping("Oignons");
    newTopping("Gros piments");
    newTopping("Ananas");
}

public long newBase(String name) {
    Base newBase = new Base();
    newBase.setName(name);
    newBase.persist();
    newBase.flush();
    return newBase.getId();
}

public long newTopping(String name) {
    Topping newTopping = new Topping();
    newTopping.setName(name);
    newTopping.persist();
    newTopping.flush();
    return newTopping.getId();
}

Voilà de quoi réaliser une initialisation automatique de données, pour favoriser la pertinence et la vélocité des recettes :

  • Les tests automatisés permettent d’aller vers une recette exhaustive et rapide, tout en donnant moins de travail aux testeurs, …
  • … qui bénéficient également d’un bon confort lors de la phase manuelle de la recette, par la capacité d’initialiser automatiquement leurs données.

J’ai présenté ici la façon de faire pour de simples tests JUnit, mais cette méthode est d’autant plus utile dans le cadre de tests d’acceptation de type Concordion par exemple.

Advertisements