La stratégie de test d'une architecture REST (2/3) - Test d’intégration

le 24/02/2009 par Benoit Guillou
Tags: Software Engineering

Cet article est le deuxième d’une série de 3 articles traitant de la stratégie de test d’une architecture REST. Il fait suite au billet sur le test unitaire d’une ressource REST. Pour rappel nous allons, par l’exemple, mettre en pace une stratégie de test sur un code d’exposition de web services REST en Java. L'exemple de code se basera sur le framework REST Jersey, implémentation de référence de Sun de la JSR-311 déjà présentée dans un précédent article . Le but de ces trois articles est de présenter un harnais de tests pouvant couvrir la mise en place de Web services REST. Ce deuxième article s’attardera sur les tests d’intégration tandis qu’un prochain article traitera des tests de recette.

USE CASE (Rappel)

RESTune est une application fictive de vente de musique en ligne. Cette application permet :

  • de lister l'ensemble des albums : GET http://.../albums
  • de récupérer un album en fournissant son identifiant : GET http://.../albums/12133
  • de créer un album : POST http://.../albums

Architecture générale de l'application (Rappel)

RESTune est une application Java classique n-tiers :

  • une couche DAO (framework Hibernate)
  • une couche Service
  • une couche Webservice (framework Jersey + JAXB pour la sérialisation / désérialisation)
  • une gestion des beans par le framework Spring

Le test d’intégration

Par définition, le test d’intégration valide la cohérence d’une fonctionnalité dans son ensemble. Dans le cas de l’implémentation d’une ressource REST, il est nécessaire de traverser toutes les couches de l’application - de l'appel HTTP au WS REST jusqu'à la base de données dans notre exemple. Cette fois, l'ensemble de notre pile logicielle Spring + Hibernate + Jaxb + Jersey sera mise en jeu. Le but n'est bien évidemment pas de tester les frameworks mais l'utilisation qui en est faite et la cohérence de l'ensemble.

Environnement léger de test d’intégration

Le dispositif présenté ci-dessus permet de disposer sur le poste développeur d’un serveur d’application (Jetty) et d’une base de données en mémoire (HSQLDB). Ces deux outils ont, en autres, la particularité d’être légers et rapides. L’intérêt de ce dispositif est de créer un contexte se rapprochant le plus possible de la réalité (serveur d’application + base de données) tout en ne sacrifiant pas les performances. Cette plateforme permet d’exécuter des tests sur le poste développeur tout en s’autorisant à passer sur une plateforme d’intégration moyennant configuration (via Maven par exemple – cf ).

La classe de test AbstractWebRunnerTestCase permet de lancer un Jetty, de déployer l'application RESTune décrite par son fichier web.xml. L’utilisation du client Jersey permet de simplifier la manipulation de ressources REST par rapport à un client HTTP tel HTTPClient. A noter que ce client, prévu initialement pour faciliter les tests unitaires du framework Jersey, est un maintenant un composant a part entière du framework. Il peut donc permettre de réaliser la partie Cliente d’une application REST, tout comme tester une autre implémentation REST que Jersey (RESTLET, RESTeasy, …).

public abstract class AbstractWebRunnerTestCase {
private static int port = 9797;
private String url;
private static Server server;
private static Client client;

protected AbstractWebRunnerTestCase() {
this.url = String.format("http://localhost:%s/", port);
}
// Helper pour manipuler les ressources
protected WebResource getResource(String relativeUrl) {
String realUrl = url + relativeUrl; return client.resource(realUrl);
}
@BeforeClass
public static void setUp() throws Exception {
server = new Server(port);

// On lance le serveur Jetty
WebAppContext webAppContext = new WebAppContext("src/main/webapp", "/");
webAppContext.setConfigurationClasses(new String[] { "org.mortbay.jetty.webapp.WebInfConfiguration",
"org.mortbay.jetty.webapp.WebXmlConfiguration", });
server.addHandler(webAppContext); server.start(); // Création d'un client Jersey client = Client.create();
}
@AfterClass
public static void tearDown() throws Exception {
if (server != null)
server.stop();
}
}

Prenons l’exemple de la recherche d’album par identifiant : GET http://.../albums/{id}

Le code à tester

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_XML)
public AlbumXmlBean getAlbumById(@PathParam("id") Long id) throws AlbumNotFoundException {
Album album = albumService.getAlbumById(id);
return new AlbumXmlBean(album);
 }

Méthode testée unitairement, il reste cependant à vérifier : - Le Verbe http autorisé (configuré par l’annotation @GET) - Le MediaType autorisé @Produces(MediaType.APPLICATION_XML) - L'utilisation des Providers / ExceptionMappers (cf JSR-311) - L’Enchainement des traitements et intégration des couches applicatives (JAXB, appel au DAO, appel à la DB).

Exemple de test d’intégration du cas Nominal

Ce test traverse l’ensemble des couches de l’application, de l’appel HTTP à l’appel à la base de données. Il permet de valider entièrement la réponse en vérifiant le code retour http et le contenu de la réponse.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class AlbumsResourceIntTest extends AbstractWebRunnerTestCase {
@Autowired
HibernateTemplate hibernateTemplate;
@Test
public void getAlbumByIdNominalCase() {
// Setup
Album kidA = new Album();
 kidA.setReleaseDate(new Date());
 kidA.setTitle("Kid A");
 hibernateTemplate.save(kidA);
 AlbumXmlBean expectedKidAXmlBean = new AlbumXmlBean();
 expectedKidAXmlBean.setId(kidA.getId());
 expectedKidAXmlBean.setTitle(kidA.getTitle());
 // Test
WebResource resource = getResource("albums/" + kidA.getId());

// Assert
// Validation du code retour
ClientResponse clientResponse = resource.get(ClientResponse.class); assertEquals(Status.OK.getStatusCode(), clientResponse.getStatus());

// Validation de l'entité contenu dans la réponse
AlbumXmlBean retrievedAlbumXmlBean = resource.get(AlbumXmlBean.class);
assertNotNull(retrievedAlbumXmlBean);
assertEquals(expectedKidAXmlBean, retrievedAlbumXmlBean);
 }
[...]
}

Les possibilités offertes par Maven Gestion des configurations

Les tests d’intégration peuvent être exécutés à deux niveaux grâce à la gestion profils Maven. En effet, Maven offre la possibilité, par simple configuration, de gérer différents environnements (développeur, intégration, recette, production par exemple).

Test d’intégration développeur

Sur le poste développeur, priorité à la rapidité d’exécution. Etant donné qu’il y a un appel HTTP + JDBC, on ne se place plus dans une problématique de test unitaire mais bien de test d’intégration. Ces tests pourraient donc être qualifiés de tests d’intégration développeur.

mvn test –Pdeveloppement

Moyennant configuration dans le POM, les tests seront exécutés dans un environnement « développement » (Jetty + HSQLDB).

Test d’intégration

Le test d’intégration se doit de s’effectuer dans un environnement le plus proche possible de celui de production. Ce profil chargera donc l’environnement d’intégration complet : on peut par exemple garder Jetty pour l’exécution des tests mais attaquer la base d’intégration (avec ses propres données, son propres schémas, …). Plus complet mais aussi plus long à tester ! Ce genre de tests pourra être évité sur le poste du développeur et être exécuté par l’automate de build (tous les soirs par exemple).

mvn test –Pintegration

Il est donc possible de séparer les tests unitaires des tests d’intégration grâce à la configuration Maven. Le plugin Jetty

mvn jetty:run

Le plugin Jetty (maven-jetty-plugin) permet de pouvoir lancer un serveur d’application léger et de déployer l’application en cours de développement. Un fois lancé, Jetty prend automatiquement en compte les modifications de code et de configuration. Cela peut notamment servir de démo pour que vous puissiez tester avec votre browser web préféré. Il est possible par exemple avec Firefox et son plugin Poster de tester le POST d’un XML sur une url.

Conclusion

Les tests d’intégration permettent de tester l’ensemble de la fonctionnalité offerte par la ressource, en invoquant l’ensemble de la pile logicielle. Ces tests sont par essence plus longs à jouer. La plateforme de test Jetty + HSQLDB est une réelle alternative permettant de tester au plus vite, dans un environnement plus riche que celui proposé par les tests unitaires. Maven offre de plus la possibilité de pouvoir piloter l’environnement sur lesquels ces tests sont exécutés par simple configuration. L’automate de build pourra alors construire l’application en exécutant au préalable les tests d’intégration sur une plateforme plus complète. Ces tests permettent de mitiger les risques sur :

  • Les codes retour et le contenu des réponses
  • L’utilisation des annotations du framework Jersey
  • MIME Type
  • L’utilisation des Providers / ExceptionMapper
  • L’URI de la ressource
  • La sérialisation / désérialisation des objets

Prochainement, nous étudierons plus en détails le test de recette d’une ressource REST. Pour être plus précis, ce dernier article exposera un moyen de réaliser et d’automatiser les tests de recette, autre moyen de valider/assurer la non-régression d’un service et d’avoir un feedback au plus tôt.

NB : Le code de RESTune est disponible ici.