La stratégie de test d'une architecture REST (1/3) - Test unitaire d’une ressource
Cet article est le premier d’une série de 3 articles traitant de la stratégie de test d’une architecture REST. Il fait suite au billet sur les types de test utilisés sur un projet Agile. Par l'exemple, nous allons mettre en place une stratégie de tests sur un code d'exposition de web services REST en Java. L'exemple de code se basera sur le framework REST Jersey, RI de Sun de la JSR-311 déjà présenté 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 premier article s’attardera sur les tests unitaires tandis que les suivants étudieront les tests d’intégration puis les tests de recette.
Use Case
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
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 unitaire
Le test unitaire est le premier maillon de la stratégie de test. Partant de la loi du Defect Cost Increase (plus un défaut est détecté tard, plus il coûte cher), le test unitaire (ou test du développeur) joue un rôle crucial. Il permet de valider que le code développé est conforme aux intentions du développeur. Partant du principe que tout test rejouable est de facto un test de non-regression, l’ensemble des tests unitaires d’une application constitue un patrimoine inestimable. Un test est par définition répétable, automatique, indépendant mais surtout pour le développeur impatient, il doit être rapide ! Nous allons donc mettre en place une stratégie de test unitaire légère et rapide. Pour rappel, Michaël Feathers définit dans Working Effectively With Legacy Code un test unitaire de la façon suivante :
- Il ne communique pas avec la base de données
- Il ne communique pas avec d’autres ressources sur le réseau
- Il ne manipule pas un ou plusieurs fichiers
- Il peut s’exécuter en même temps que les autres tests unitaires
- On ne doit pas faire quelque chose de spécial, comme éditer un fichier de configuration, pour l’exécuter
Le pattern "couche d’exposition fine"
Le test unitaire se borne à valider la couche d'exposition de services. Dans les applications Web classiques, il est préférable de ne laisser que très peu de logique au niveau de la couche présentation. D’un point de vue design, l’application y gagne en lisibilité et en testabilité. Il est facile de faire le parallèle avec la couche d’exposition. Plus elle sera fine et ne comportera des données propres à l’exposition, plus elle sera simple à tester. En REST, l'exposition de services se fait bien entendu au moyen de ressources.
La JSR-311 se basant sur des beans annotés, il est simple de tester unitairement ces beans hors de tout contexte. Nous allons donc tester notre ressource Album en isolation. Le code d’exposition de la ressource est le suivant :
@Singleton @Path("/albums") public class AlbumsResource {
@Context private UriInfo uriInfo; @Inject AlbumService albumService; [...] @POST @Produces(MediaType.APPLICATION_XML) @Consumes(MediaType.APPLICATION_XML) public Response postAlbum(AlbumXmlBean albumXmlBean) { Album album = albumXmlBeanToAlbum(albumXmlBean); try { albumService.save(album); } catch (AlbumAlreadyExists e) { return Response.status(Status.CONFLICT).entity("Album with title " + album.getTitle() + " already exists").build(); } AlbumXmlBean albumXmlBeanAfterSave = albumToAlbumXmlBean(album); // Building URI UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder(); URI uri = uriBuilder.path(albumXmlBeanAfterSave.getId().toString()).build(); return Response.created(uri).entity(albumXmlBeanAfterSave).build(); } [...] }
Le pattern Mock Object
Le pattern Mock Object ou bouchonnage permet d’isoler ma ressource Album de ses dépendances et de se conformer à la définition de test unitaire faite par Feathers. Nous allons donc mocker les couches inférieures grâce au framework Unitils. Unitils est un framework qui permet de simplifier les tests unitaires. Il permet entre autres de simplifier l’écriture de Mock grâce à EasyMock.
@RunWith(UnitilsJUnit4TestClassRunner.class) public class AlbumsResourceUTest { // Objet du test @TestedObject AlbumsResource albumsResource;
// Mock + injection dans l'objet du test @RegularMock @InjectIntoByType AlbumService albumService;
// Mock + injection dans l'objet du test @RegularMock @InjectIntoByType UriInfo uriInfo;
// Simple mock @Mock UriBuilder uriBuilder; [...] }
Easymock permet en un minimum de code de disposer d'objet mocké que l'on va pouvoir maîtriser afin de mener à bien nos tests. Pour information, la méthode EasyMock.expect(…)
permet de valider l'utilisation des objets mockés (nombre d'appel, arguments, …) tout en manipulant leur comportement.
Exemple : Test unitaire du cas nominal
Ce test permet de valider le comportement nominal du service. Il vérifie la réponse de la ressource, le code retour ainsi que l'appel à la méthode sous-jacente albumService.save()
.
@Test public void postAlbumNominalCase() throws URISyntaxException { // Setup Album album = new Album(); album.setTitle("In Rainbows"); album.setId(new Long(32)); albumXmlBean.setTitle("In Rainbows"); albumXmlBean.setId(album.getId()); URI expectedUri = new URI("http://test");
// Mock expectations albumService.save(album); EasyMock.expect(uriInfo.getAbsolutePathBuilder()).andReturn(uriBuilder); EasyMock.expect(uriBuilder.path(album.getId().toString())).andReturn(uriBuilder); EasyMock.expect(uriBuilder.build()).andReturn(expectedUri); EasyMockUnitils.replay();
// Test Response response = albumsResource.postAlbum(albumXmlBean);
// Asserts assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); AlbumXmlBean returnedAlbumXmlBean = (AlbumXmlBean) response.getEntity(); assertTrue(response.getMetadata().containsKey("Location"));
URI returnedUri = (URI) response.getMetadata().get("Location").get(0); assertEquals(expectedUri, returnedUri); assertEquals(albumXmlBean, returnedAlbumXmlBean);
Il est dès lors possible de construire tous les cas de test en pilotant ces mocks. Par exemple le test suivant valide qu’à la levée d’une exception AlbumAlreadyExistsException
, le web service renvoie un code retour CONFLICT (409) :
Conclusion
Les tests unitaires sur un service web permettent de tester en isolation une ressource (sans Spring + Hibernate + Jaxb + Jersey). Quand on connait le contexte que traine chacun de ces frameworks, on s'aperçoit qu'on arrive à tester au maximum notre ressource de manière simple (manipulation de POJOs). Ces tests seront d’autant plus fréquemment rejoués qu’ils sont rapides à exécuter et participeront donc à la qualité générale de l’application. A travers ces tests, il est possible de valider que la ressource mise en place est conforme aux intentions du développeur :
- en vérifiant l'appel aux méthodes sous-jacente (vérification de l'appel de méthodes, des arguments)
- en simulant les exceptions et en validant le comportement de la couche d'exposition
- en validant les codes retours et le contenu des réponses répondant aux différents use cases du service
Le test unitaire est le premier maillon du harnais de tests. Nous verrons dans les articles suivants dans quelle mesure les tests d'intégration et les tests de recette participent eux aussi à la mise en place d'une stratégie de test efficace.
Rq : Vous pouvez me contacter si vous désirez le code complet de RESTune