Les jeux de données font partie intégrante des tests. Elaborer un jeu de données demande une connaissance fonctionnelle, aussi bien sur la nature des données que sur le scénario de test envisagé. Utiliser des jeux de données réalistes participe à la compréhension du scénario de test et, donc, à sa documentation.
S’il vous était possible de générer ces fameux jeux de données, seriez-vous intéressés ?C’est précisément l’objet de ce billet et d’un modeste outil baptisé JavaBean Marshaller.
Continuer la lecture
Archives par mot-clé : Test
Tester unitairement une application Java
Un récent 13-14 m’a donné l’occasion de partager ma vision des tests unitaires avec mes collègues, qu’ils soient développeurs Java ou chefs de projet.
Au cours de cette présentation, j’ai essayé de répondre à des questions qui font régulièrement débat : « Qu’est-ce qu’un test unitaire ? A quoi çà sert ? Que dois-je tester ? ». Tester n’est pas facile. Cela demande un apprentissage. Heureusement, il existe des bonnes pratiques et des outils. J’y ai notamment présenté ceux faisant parti de notre stack technique : JUnit, Mockito, DbUnit et Spring Test.
Des exemples de code ont illustré cette présentation dont voici librement le support :
Spring Core 4.2 Certification Mock Exam
Four years ago, I’ve published a first mock exam for the Spring Core 3.0 Certification. Encouraged by Michael and Alan, I’ve updated this free mock exam for the Spring Professional certification based on the Spring Core 4.2 course.
According to the Core Spring 4.2 Certification Study Guide, 3 new topics have been added to the Spring Core 4.2 mock exam: REST, Microservices and Spring Cloud. They replace older topics: JMX, JMS and Remoting.
Démystifier l’annotation @SessionAttributes de Spring MVC
Le développement d’applications web requière une vigilance toute particulière quant à l’utilisation de la session web. Spring MVC offre les mécanismes permettant aux développeurs de ne plus manipuler directement l’objet HttpSession mis à disposition par le conteneur web. Les 2 annotations @Scope(« session ») et @SessionAttributes en font parties. Dans ce billet, je vous expliquerai le fonctionnement de l’annotation @SessionAttributes qu’il est essentiel de maitriser avant d’utiliser. Nous verrons qu’elle fonctionne de pair avec l’annotation @ModelAttribute et qu’elle permet de simuler une portée conversation. Nous commencerons cet article par rappeler ce qu’est un modèle et nous le terminerons en testant unitairement du code qui utilise @SessionAttributes.
Continuer la lecture
Tester le code JavaScript de vos webapp Java
Vous développez une application web en Java. Le couche présentation est assurée typiquement par un framework MVC situé côté serveur : Spring MVC, Struts 2, Tapestry ou bien encore JSF. Votre projet est parfaitement industrialisé : infrastructure de build sous maven, intégration continue, tests unitaires, tests Selenium, analyse qualimétrique via Sonar.
A priori, vous n’avez rien à envier à la richesse grandissante de l’écosystème JavaScript, de l’outillage et des frameworks MV* côté clients. Et pourtant, quelque chose vous manque cruellement. En effet, depuis que RIA et Ajax se sont imposés, votre application Java contient davantage de code JavaScript qu’il y’a 10 ans. S’appuyant sur des librairies telles que jQuery ou Underscore, ce code JavaScript est typiquement embarqué dans votre WAR. Pour le valider, les développeurs doivent démarrer leur conteneur web et accéder à l’écran sur lequel le code est utilisé. Firebug ou Chrome sont alors vos meilleurs amis pour la mise au point du script.
Ce code JavaScript n’est généralement pas documenté. Le tester manuellement demande du temps. Les modifications sont sources d’erreur. Tout changement est donc périlleux. Si, à l’instar de vos tests JUnit pour vous classes Java, vous disposiez de tests JavaScript, vous en seriez comblés. Or, c’est précisément ce qu’il vous manque. Et c’est là où Jasmine et son plugin maven viennent à votre rescousse.
Continuer la lecture
Développer et industrialiser une web app avec AngularJS
Au travers du billet Elastifiez la base MusicBrainz sur OpenShift, je vous ai expliqué comment indexer dans Elasticsearch et avec Spring Batch l’encyclopédie musicale MusicBrainz. L’index avait ensuite été déployé sur le Cloud OpenShift de RedHat.
Une application HTML 5 était mise à disposition pour consulter les albums de musique ainsi indexés. Pour m’y aider, Lucian Precup m’avait autorisé à adapter l’application qu’il avait mise au point pour l’atelier Construisons un moteur de recherche de la conférence Scrum Day 2013.
Afin d’approfondir mes connaissances de l’écosystème JavaScript, je me suis amusé à recoder cette application front-end en partant de zéro. Ce fut l’occasion d’adopter les meilleures pratiques en vigueur : framework JavaScript MV*, outils de builds, tests, qualité du code, packaging …
Au travers de ce article, je vous présenterai comment :
- Mettre en place un projet Anguler à l’aise d’Angular Seed, Node.js et Bower
- Développer en full AngularJS et Angular UI Bootstrap
- Utiliser le framework elasticsearch-js
- Internationaliser une application Angular
- Tester unitairement et fonctionnellement une application JS avec Jasmine et Karma
- Analyser du code source JavaScript avec jshint
- Packager avec Grunt le livrable à déployer
- Utiliser l’usine de développement JavaScript disponible sur le Cloud : Travis CI, Coversall.io et David
Le code source de l’application est bien entendu disponible sur GitHub et testable en ligne.

Démarrer un projet avec Angular Seed
Hébergé sur GitHub et maintenu par les auteurs d’Angular, le projet angular-seed permet de démarrer rapidement une application Angular. Outre le squelette applicatif, ce projet propose :
- des exemples de tests unitaires et de tests dits end-to-end,
- des scripts .sh ou .bat permettant d’exécuter ces différents types de tests
- un script JS permettant de démarrer un serveur web sous NodeJS
Le README.MD explique de manière approfondie l’organisation du projet et la nature de chaque fichier.
Une fois ce repository cloné sous GitHub ou bien dézippé en local, il est possible de le personnaliser à sa guise.
Une alternative à angular-seed serait d’utiliser Yo pour générer le squelette de l’application, sur un principe similaire aux archetypes maven.
Exécuter l’application
L’application blanche fournie dans Angular Seed étant une application full HTML, il n’est pas nécessaire de la déployer dans un serveur d’application JEE ou un conteneur web. Un simple serveur web comme Apache ou Nginx est nécessaire.
Les utilisateurs de Firefox pourront même se passer de serveur web et ouvrir directement le fichier app/index.html à partir de leur disque.
Chrome n’ayant pas cette faculté (les requêtes Ajax chargées de récupérer un fichier sur disque sont bloquées), vous pouvez utiliser le serveur web installé sur votre poste de développement.
En guise de serveur web, vous pourrez utiliser le script scripts\web-server.js pour en démarrer un en full JavaScript. Le seul pré-requis est l’installation de NodeJS qui intègre le moteur JavaScript V8 de Google. A noter que NodeJS et son gestionnaire de paquets npm seront nécessaires dans la suite de cet article pour installer les outils, construire l’application, monter de version les dépendances ou bien encore exécuter les tests.
Une fois NodeJS installé et ajouté au PATH du système, exécuter la commande suivante pour démarrer le serveur:
D:\Dev\angular-musicbrainz>node scripts\web-server.js Http Server running at http://localhost:8000/
Saisir l’URL http://localhost:8000/app/index.html dans le navigateur de votre choix. Les requêtes HTTP apparaissent sur la console :
GET /app/index.html Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 GET /app/lib/bootstrap/dist/css/bootstrap.css Mozilla/5.0d (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 GET /app/lib/bootstrap/dist/css/bootstrap-theme.css Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/53 GET /app/lib/angular-resource/angular-resource.js Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537. …
Structuration du projet
La structure des répertoires du projet reprend celle d’Angular Seed. Le tableau ci-dessous liste les librairies tierces utilisées par l’application.
Répertoires / fichiers | Description |
app/ | Code source et ressources de l’application |
css/ | Feuilles de styles CSS |
i18n/ | Fichiers JSON de traduction de l’application |
img/ | Images et icône |
index.html | Page principale de la Single Page Application |
js/ | Fichiers JavaScript spécifiques à l’application |
app.js | Déclaration des modules et démarrage de l’application |
controllers.js | Contrôleurs Angular spécifiques à l’application |
directives.js | Directives Angular spécifiques à l’application |
filters.js | Filtres / formateurs Angular spécifiques à l’application |
routes.js | Configuration des routes |
services.js | Services spécifiques à l’application |
lib/ | Librairies tierces déclarées dans bower.json |
angular/ | Module principal d’Angular |
angular-i18n/ | Fichiers de traductions fournis par Angular |
angular-mocks/ | Mocks permettant de bouchonner des services Angular |
angular-resource/ | Accès REST aux ressources serveur |
angular-route/ | Routage des vues en fonction de l’URL |
angular-sanitize/ | Filtres standards |
angular-scenario/ | DSL des scénarios de tests end-to-end |
angular-ui-boostrap-bower/ | Widgets Angular basés sur Bootstrap |
bootstrap/ | Mise en page et charte graphique |
elasticsearch-js/ | Client JavaScript pour Elasticsearch |
jquery/ | Manipulation de DOM |
partials/ | Template HTML des vues de l’application |
directives/ | Template HTML des directives de l’application |
conf/ | Configuration Karma des tests unitaires et e2e |
dist/ | Répertoire de destination du livrable de production |
node_modules/ | Modules NodeJS utilisées par Grunt |
scripts/ | Scripts shell, JS et batch permettant de lancer les tests et de démarrer un serveur web |
test/ | Code source des tests |
e2e/ | Tests end-to-end |
unit/ | Tests unitaires |
Automatisation avec Grunt
Grunt peut être comparé au Gradle du monde JavaScript. Il permet d’exécuter des tâches sous NodeJS et est particulièrement utile pour automatiser certaines tâches de développement. Son installation nécessite une seule commande :
npm install -g grunt-cli
Le gestionnaire npm permet ensuite de télécharger et d’installer les modules NodeJS nécessaires au fonctionnement du script Grunt. Commande à exécuter à la racine du répertoire, au même niveau que le fichier package.json :
npm install
Le sous-répertoire node_modules est alimenté par les modules déclarés dans le fichier package.json.
Angular Seed ne configurant pas Grunt, je me suis inspiré de différents exemples pour mettre au point le script Gruntfile.js.
Voici quelques commandes utiles :
- grunt test : lance successivement les tests unitaires et les tests end-to-end
- grunt server : démarre un serveur web, ouvre la page dans l’application et, à l’instar de JRebel, recharge à chaud le code modifié depuis votre IDE.
- grunt jshint : vérifie que la qualité du code de production JavaScript
- grunt build : construit le livrable à installer sur le serveur web. Les fichiers générés sont mis à disposition dans le sous-répertoire dist.
- grunt karma:coverage : génère le taux de couverture des tests unitaires. Au format HTML, le rapport est accessible depuis le sous-répertoire coverage\PhantomJS 1.9.2 (Windows 7)\lcov-report
Pendant le développement de l’application web, à des fins de débogage, le code JavaScript est non minifié et séparé dans plusieurs fichiers, de même pour les feuilles de style CSS.
Sur un principe similaire à ce que propose Jawr dans le monde Java, l’étape de build va permettre d’obtenir un livrable le plus léger possible. Voici les opérations effectuées :
- Le code JavaScript et les feuilles de styles CSS sont concaténées puis minifiés. Exemple de directive à placer dans le code HTML :
Directive build:js sur la page index.html
- Les images bitmaps et vectorielles sont réduites.
- Les pages HTML référençant ces ressources statiques sont mises à jour en conséquences.
Dépendances Bower
Comme vu au paragraphe précédent, les modules nécessaires au fonctionnement de l’infrastructure de build basé sur Grunt sont récupérés à l’aide de npm et du fichier package.json.
Les librairies nécessaires au développement de l’application (Angular, Bootstrap) et de ses tests sont gérées par un second système de dépendances, à savoir Bower. Cet outil permet de rechercher des librairies via la commande bower search <nom-librairie>. L’instruction bower install <nom- librairie> –save(-dev) permet de la télécharger et de la référencer dans le fichier bower.json. Les librairies sont installées dans le répertoire app/lib configuré dans le fichier .bowerrc.
S’appuyant sur le fichier bower.json, la commande bower install permet de récupérer toutes les dépendances utilisées par l’application. Elle permet également de mettre à jour les dépendances lors d’une montée de version.
En théorie, il n’est pas nécessaire d’archiver ces dépendances dans le gestionnaire de code source (ici GitHub). En pratique, c’est utile de pouvoir exécuter l’application sans avoir à installer ni NodeJS ni Bower.
Architecture applicative
En avril 2013, je découvrais Angular à Devoxx France. Je vous en présentais ici même les mécanismes et les fonctionnalités clés. Après m’être auto-formé et avoir animé un workshop sur ce framework, j’étais impatient de le mettre en œuvre sur un projet personnel (en attendant un futur projet pro). Et autant vous l’annoncer dès à présent, je n’ai pas été déçu : j’ai pris un réel plaisir à développer le front end de mon application de recherche angular-musicbrainz.
D’un point de vue de l’architecture applicative, cette single page application est composée de 3 fichiers HTML principaux :
- index.html : bootstrape l’application en initiant le gabarit HTML (essentiellement le menu) et en téléchargeant les fichiers JavaScript, HTML et CSS nécessaires à son fonctionnement
- partials\search.html : vue de recherche et de restitution des résultats. C’est l’implémentation de cette vue que nous allons présenter dans la suite de se billet.
- partials\info.html : vue référençant quelques URLs utiles.
Le code JavaScript de l’application est découpé techniquement. A une couche technique (contrôleurs, services, filtres et directives) correspond un fichier JavaScript et un module Angular.
Simple, cette organisation est pratique pour de petites applications. Sur des applications plus conséquences, un découpage fonctionnel est à privilégier.
Services
L’application repose sur 4 services implémentés dans le fichier services.js :
Nom du service | Fonctionnalités |
es | Crée et configure un client Elasticsearch. S’appuie sur la fabrique mise à disposition par la librairie elasticsearch-js. |
searchService | Code métier regroupant les requêtes JSON de recherche Elasticsearch. 2 méthodes sont disponibles : l’une pour la recherche full text et l’autre pour l’auto-complétion. |
userLanguage | Permet d’accéder à la locale de l’utilisateur. |
translation | Récupère le fichier de traduction de l’application. |
Filtres
Les templates HTML reposent sur 5 filtres personnalisés
Pour les détails d’implémentation, se référer au code source filters.js et aux tests unitaires qui les documentent filterSpecs.js.
Directives
2 widgets graphiques ont été réalisés à l’aide de directives :
A noter que la directive rating d’Angular Bootsrap offre une alternative à la directive rank. Cette dernière reprend la CSS de MusicBrainz permettant d’ajuster le dégradé des étoiles au pixel près.
Afin de rendre le code des directives plus lisible et maintenable, les templates HTML de ces 2 directives ont été externalisés dans des fichiers html dédiés.
Exemple rating.xml :
<span class="inline-rating">
<span class="star-rating small-star">
<span style="width:{{score+ceil}}%;" class="current-rating">{{score}}</span>
</span>
</span>
La configuration des tests unitaires Karma a dû être ajustée en conséquences :
// generate js files from html templates to expose them during testing.
preprocessors : {
'app/partials/directives/**/*.html': ['ng-html2js']
},
Contrôleurs
A chaque vue de l’application, correspond un contrôleur. Le fichier controller.js en défini donc deux : SearchCtrl et InfoCtrl.
Trivial, le contrôleur InfoCtrl met à disposition dans le scope de le vue info les 2 URLs affichées et formatées à l’aide de la directive Angular ngLinky
Contrôleur :
$scope.demoUrl = 'http://angular-musicbrainz.javaetmoi.com/';
Template :
<li>Online Demo: <span ng-bind-html="demoUrl | linky"/></li>
Le contrôleur SearchCtrl embarque toute la logique applicative de l’application. Il offre à la fois des fonctions réagissant aux actions utilisateurs et les données utilisées par Angular lors du rendu de la vue search. En voici les principales :
Propriété | Type | Description |
fullTextSearch | fonction | Exécute une recherche full text lors du clic sur le bouton « Recherche MusicBrainz». |
autocomplete | fonction | Exécute une requête d’auto-complétion à chaque frappe de l’utilisateur dans le zone de recherche. |
selectPage | Fonction | Permet à l’utilisateur de sélectionner une plage de résultats. Exécute une recherche Elasticsearch sur la plage indiquée. |
searchResp | Donnée | Résultats Elasticsearch d’une recherche fulltext. |
pageSize | Donnée | Nombre de résultats à afficher à l’écran. |
currentPage | Donnée | Plage de résultats actuellement affichée. |
pageSizes | Donnée | Tailles de plages que l’utilisateur peut choisir. |
Routes
Par rapport au template angular-seed, la configuration des routes a été externalisée dans un fichier dédié routes.js.
Application
Le 6ième et dernier module Angular correspond au module applicatif musicAlbumApp déclaré dans le fichier app.js. Outre la déclaration des modules Angular nécessaires au fonctionnement de l’application, ce module est chargé de déterminer la langue dans laquelle l’interface doit s’afficher puis charger les données adéquates. Nous y reviendrons dans la suite de cet article.
Utilisation d’elasticsearch-js
Pour interroger le cluster Elasticsearch depuis le navigateur, j’ai étudié 3 possibilités : le service natif $http d’Angular, la librairie elastic.js et la librairie elasticsearch-js. Sortie en décembre 2013 et mise en avant par Elasticsearch.org, j’ai choisi d’utiliser cette dernière. Via la création de tickets GitHub, j’ai eu la chance de pouvoir contribuer à l’amélioration de cette jeune librairie déjà très mature.
Le module esFactory permet de déclarer en quelques lignes un client JavaScript Elasticsearch. Voici les paramètres renseignés :
angular.module('musicAlbumApp.services', ['ngResource'])
.value('version', '1.0')
// elasticsearch.angular.js creates an elasticsearch
// module, which provides an esFactory
.service('es', ['esFactory', function (esFactory) {
return esFactory({
hosts: [
// you may use localhost:9200 with a local Elasticsearch cluster
'es.javaetmoi.com:80'
],
log: 'trace',
sniffOnStart: false
});
}])
J’ai volontairement désactivé la fonctionnalité de sniffOnStart. En effet, j’ai configuré le reverse proxy Nginx pour ne laisser passer que les requêtes de _search. Les requêtes HTTP de type HEAD envoyées par le client pour déterminer la disponibilité des différents nœuds du cluster étaient donc rejetées.
L’appel au service de recherche Elasticsearch est également très simple. Dans l’attribut body de la fonction search proposée par l’API, est utilisé le formalisme standard de déclaration des requêtes au format JSON. En complément, les attributs index et type permettent respectivement d’indiquer sur quel index Elasticsearch et sur quel type de document lancer la recherche. Voici un exemple d’appel :
Extrait méthode fullTextSearch
.factory('searchService', ['es', function (es) {
return {
'fullTextSearch': function (from, size, text) {
return es.search({
index: 'musicalbum',
type: 'album',
body: {
'from': from,
'size': size,
'query': {
'bool': {
'must': [
{
'fuzzy_like_this': {
'fields': [
'name',
'artist.name',
'year.string'
],
'like_text': text,
'min_similarity': 0.7,
'prefix_length': 1
}
}
]
}
},
'facets': {
'artist_type': {
'terms': {
'field': 'artist.type_id'
}
},
'album_rating': {
'histogram': {
'key_field': 'rating.score',
'interval': 21
}
},
'album_year': {
'range': {
'field': 'year',
'ranges': [
{ 'to': 1970},
{ 'from': 1970, 'to': 1980},
{ 'from': 1980, 'to': 1990},
{ 'from': 1990, 'to': 2000},
{ 'from': 2000, 'to': 2010},
{ 'from': 2010 }
]
}
}
}
}
});
},
La fonction search renvoie une promesse de réponse. Pour récupérer la réponse retournée par Elasticsearch, la méthode then peut être utilisée :
searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
function (resp) {
$scope.searchResp = resp;
$scope.totalItems = resp.hits.total;
searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
function (resp) {
$scope.searchResp = resp;
$scope.totalItems = resp.hits.total;
}
);
Localisation
Le service $locale d’Angular permet de formater les nombres et les dates en fonction des préférences linguistiques de l’utilisateur. Il existe autant de fichiers JavaScript que de combinaisons langue / pays (exemples : angular-locale_fr-fr.js, angular-locale_en-us.js).
Pour charger le fichier adéquat, l’application doit détecter le langage défini par l’utilisateur dans son Navigateur. A première vue, les variables du DOM window.navigator.userLanguage et window.navigator.language auraient dû apporter cette information. Il en aurait été trop simple. L’article Detecting a Browser’s Language in Javascript explique précisément pourquoi.
Le header HTPP Accept-Language ne peut être lue que côté serveur web. Or, l’application était jusque-là full JavaScript. Convertir la page index.html en une page PHP ou JSP aurait été simple. Néanmoins, j’ai préféré m’affranchir de toute installation côté serveur. J’ai donc utilisé le service http://ajaxhttpheaders.appspot.com mis à disposition sur Google App Engine et dont voici un exemple d’utilisation :
$http.jsonp('http://ajaxhttpheaders.appspot.com?callback=JSON_CALLBACK').
success(function (data) {
var acceptLang = data['Accept-Language'];
langRange = userLanguage.getFirstLanguageRange(acceptLang);
language = userLanguage.getLanguage(langRange);
if (sessionStorage) {
sessionStorage.setItem('userLanguageRange', langRange);
}
}).
finally(function () {
loadI18nResources();
});
Une fois la langue de l’utilisateur connue, la fonction $.getScript de JQuery permet de charger dynamiquement le fichier JavaScript Angular correspondant.
Afin d’éviter des appels intempestifs au service http://ajaxhttpheaders.appspot.com, la langue est conservée dans le sessionStorage du navigateur.
Dans un souci d’internationalisation, l’application angular-musicbrainz a été traduite en 2 langues : le français et l’anglais. Les libellés affichés à l’écran dépendent donc des préférences utilisateurs. Le système mis en œuvre s’inspire de ce que proposent les articles Creating multilingual support using AngularJS et Traduction des libellés dans les vues AngularJS. Un objet translation contenant la traduction de tous les libellés d’une langue est chargé à partir d’un fichier JSON puis ai mis dans le scope parent ($rootScope). Cet objet peut être accédé à la fois dans les templates HTML que côté JavaScript :
<label id="search-input-label" class="col-sm-3 control-label" ng-bind="translation.SEARCH_LABEL">Searching a music album</label>
$scope.pageSizes = [
{count: 5, label: '5 ' + $scope.translation.SEARCH_PAGE_RESULT},
Widgets graphiques
Le projet UI Bootstrap propose une douzaine de directives Angular basées sur Boostrap : sélection de date avec calendrier, accordéon, onglets, barres de progression, fenêtre popup, collapse, carrousel d’images …
Notre web app de recherche utilise 3 de ses directives :
Comme le montre les exemples ci-dessus, les directives UI Boostrap permettent d’étendre le HTML, soit par de nouveaux tags (ex: <pagination/> , soit par des attributs enrichissants des tags standards (ex : typeahead sur <input/> ).
Tests unitaires
Avec son découpage en modules, la possibilité de créer des mocks et l’indépendance du code JavaScript au regard du DOM, Angular permet de tester unitairement chaque contrôleur, service, filtre, route et directive. Qui plus est, l’application blanche angular-seed vient avec toute l’infrastructure de tests : spécifications Jasmine à compléter, configuration Karma, scripts batch et shell permettant d’exécuter les tests. Autant dire, le développeur n’a aucune excuse pour ne pas tester unitairement son application.
L’application angular-musicbrainz comptabilise 36 tests unitaires, couvrant ainsi 65% du code source. Avant de pouvoir exécuter les tests unitaires, il est nécessaire d’installer les quatre modules Karma suivants :
npm install -g karma karma-junit-reporter karma-ng-html2js-preprocessor karma-coverage
Les tests unitaires peuvent être exécutés de 2 manières :
- Par la commande grunt : grunt karma
- Ou par un script : scripts\test.bat
Karma exécute les tests puis se met en attente de changements. En effet, tel infinitest, Karma relance les tests à chaque modification du code source ou des tests. Cela s’avère très pratique pour lever au plus tôt toute régression ou bien travailler en TDD.
Autre aspect de Karma : il permet de faire tourner les tests simultanément dans un ou plusieurs navigateurs. Dans le fichier de configuration karma.conf.js, Google Chrome et le navigateur headless PhantomJS ont été retenus.
Une fois la structuration d’un cas de test prise en main (mots clés describe, beforeEach et it), l’écriture du code de tests est plus ou moins simple. La difficulté principale vient de la lourdeur de la configuration nécessaire à mettre en place pour bouchonner les adhérences. Voici par exemple comment tester la fonction fullTextSearch du contrôleur SearchCtrl :
it('fullTextSearch should put the searchResp variable into the scope', function () {
expect(scope.searchResp).toBeUndefined();
expect(scope.isAvailableResults()).toBeFalsy();
expect(scope.isAtLeastOneResult()).toBeFalsy();
scope.fullTextSearch('U2', 1);
it('fullTextSearch should put the searchResp variable into the scope', function () {
expect(scope.searchResp).toBeUndefined();
expect(scope.isAvailableResults()).toBeFalsy();
expect(scope.isAtLeastOneResult()).toBeFalsy();
scope.fullTextSearch('U2', 1);
// scope.$digest() will fire watchers on current scope,
// in short will run the callback function in the controller that will call anotherService.doSomething
scope.$digest();
expect(scope.searchResp).toBeDefined();
expect(scope.totalItems).toBeDefined();
expect(scope.isAvailableResults()).toBeTruthy();
expect(scope.isAtLeastOneResult()).toBeTruthy();
});
Le contrôleur SearchCtrl s’appuie sur le service searchService dont la fonction fullTextSearch a dû être bouchonnée. Au final, le développeur écrit plus de code de test que de code testé.
Espérons que le duo Karma / Jasmine gagnera en maturité avec le temps. En Java, l’utilisation des annotations @Mock et @InjectInto permet en effet de réduire drastiquement ce type code.
Non des moindre, le dernier point à connaître lors de l’écriture des tests concerne les assertions. Venant avec un nombre de matchers clés en mains, Jasmine permet d’écrire ses propres matchers.
Tests end-to-end
A l’instar de ce que propose Selenium dans le monde Java, Karma permet d’écrire et d’exécuter des scénarios fonctionnels. Un prérequis à leur exécution est que l’application web doit être démarrée.
Là encore, l’application blanche angular-seed vient avec toute l’infrastructure de tests e2e nécessaire. Comme prérequis, le module karma-ng-scenario doit être installé via npm :
npm install -g karma-ng-scenario
Les tests e2e peuvent être exécutés en ligne de commande: scripts\e2e-test.bat
Pour l’écriture des scénarios de tests, le module angular-scenario fournit un DSL permettant de sélectionner des éléments du DOM et de simuler des évènements utilisateurs. A noter que le framework Protactor doit remplacer à termes ce module.
Comme le montre l’extrait de code ci-dessous, le code reste lisible :
describe('search', function () {
beforeEach(function () {
browser().navigateTo('#/search');
});
it('should render search when user navigates to /search', function () {
expect(element('#search-input-label').text()).
toContain('music');
});
it('U2 album search', function () {
input('searchText').enter('U2');
element(':button').click();
expect(element('#result-number').text()).
toContain('22');
});
});
Créé par l’un des développeurs d’Angular, Karma a l’avantage de connaître le fonctionnement interne d’Angular. Cette faculté lui permet de résoudre les problèmes de requêtes Ajax souvent rencontrés dans les tests Selenium. Adieux les tempos ou autre waitForElement.
Exécution des tests end-to-end dans Chrome :
Contrôle qualité avec JSHint
Fork actif de jslint, JSHint s’apparente au Checkstyle du monde Java. Cet outil Open Source effectue plusieurs types de vérifications sur les fichiers JavaScript :
- Conventions de nommage
- Règles de formatage
- Bonnes pratiques permettant d’éviter de potentiels bugs
Le fichier de configuration .jshintrc permet d’activer chacune des dizaines de règles proposées par JSHint. Activée sur notre projet, la règle curly vérifie par exemple s’il ne manque pas des accolades dans les boucles et les conditions.
La vérification des fichiers JavaScript peut ensuite se faire, soit en ligne de commande :
D:\Dev\angular-musicbrainz>grunt jshint Running "jshint:all" (jshint) task Linting app/js/services.js ...ERROR [L140:C17] W116: Expected '{' and instead saw 'return'. return undefined;
Soit directement depuis IntelliJ IDEA après configuration :
JSHint a toute sa place sur un projet de grande taille sur lesquels de nombreux développeurs travaillent puis se relaieront pour sa maintenance. Sur de plus modestes applications comme angular-musicbainz , il a le mérite de former et de mettre en garde des développeurs JavaScript Junior.
Usine de dév JavaScript
Dans le billet Ma petite usine logicielle, je vous expliquais comment utiliser CloudBees et GitHub pour industrialiser vos projets Java. Vous l’aviez compris, l’intégration continue et l’automatisation des tâches me tiennent à cœur. J’ai donc naturellement regardé ce qui existait dans le monde JavaScript. Ce dernier n’est pas en reste. Voici ce que j’ai mis en place sur angular-musicbrainz.
Déjà mis en place sur mes projets Java avec Maven, Travis CI est une plateforme d’intégration continue mise à disposition gratuitement pour les projets Open Source. Cette plateforme présente l’avantage de supporter NodeJS et peut donc intégrer des applications JavaScript.
La configuration du build Travis se trouve dans le fichier .travis.yml :
language: node_js
node_js:
- 0.10
before_script:
- npm install -g grunt-cli
script:
- grunt karma:ci
after_success:
- grunt coverage
On demande à Travis d’installer le client Grunt avant d’exécuter les tests unitaires et de publier la couverture de code. A chaque commit dans le repo GitHub, le build est lancé. La sortie console s’affiche en temps réel :
En cas d’échec du build, vous pouvez être notifiés par email , IRC, webhook …
Outre la génération d’un rapport de couverture de code testé, la commande grunt coverage envoie ce rapport au service Coveralls. Ce dernier historise le taux de couverture et offre une IHM permettant de naviguer parmi les fichiers analysés.
L’historique des builds du projet angular-musicbrainz est accessible en ligne :
Le taux de couverture du build n°494581 est également consultable en ligne :
Autre service en ligne intéressant : pouvoir vérifier rapidement que les dépendances d’une application sont à jour. C’est ce que propose David. Voici visuellement la synthèse proposée par David pour les dépendances de dev d’angular-musicbainz :
A noter qu’un service similaire pour les dépendances utilisées par Bower serait intéressant.
Chacun de ces services propose un badge dynamique. Pratique, ces badges peuvent être affichés dans le README.MD :

Conclusion
Ce long billet m’aura permis de vous faire découvrir les différentes facettes du monde JavaScript dont j’ai fait connaissance tout au long du développement de cette petite application web de recherche.
L’utilisation d’Angular est plaisante et me réconcilie avec le développement JavaScript que je ne trouvais jusque-là pas assez industrialisé. Diminuant le nombre de lignes de code JavaScript au profit du HTML, ce framework permet de structurer proprement le code JavaScript. Ceux qui ont connus des applications où chaque page comporte des centaines de lignes jQuery non organisées apprécieront sans aucun doute.
En quelques années, je constate avec plaisir que l’écosystème JavaScript a rattrapé son retard sur celui de Java : intégration continue, outils de builds, tests unitaires, tests fonctionnels, qualimétrie, gestion des dépendances, MVC, data-binding, POJO, templating, injection de dépendances, modularisation, nombre grandissant de frameworks, moteur d’exécution optimisé, support IDE … Chapeau bas.
DbSetup, une alternative à DbUnit
Lors du développement de tests d’intégration, j’ai récemment eu besoin de charger une base de données à l’aide de jeux de données. Pour écrire mon premier test, j’ai simplement commencé par écrire un fichier SQL. En un appel de méthode (JdbcTestUtils::executeSqlScript) ou une ligne de déclaration XML (<jdbc:script location= » » />), Spring m’aidait à charger mes données.
Pour tous ceux qui se sont déjà prêtés à l’exercice, maintenir des jeux de données est relativement fastidieux, qui plus en SQL. Cette solution n’était donc pas pérenne.
Depuis une dizaine d’années, j’utilise régulièrement DbUnit pour tester la couche de persistance des applications Java sur lesquelles j’interviens, qu’elle soit développée avec JDBC, Hibernate ou bien encore JPA. Cette librairie open source est également très appréciable pour tester unitairement des procédures stockées manipulant des données par lot. Pour mon besoin, j’aurais donc pu naturellement me tourner vers cet outil qui a fait ses preuves et dont je suis familier.
Mais voilà, commençant à apprécier les avantages de la configuration en Java offerte par Spring et les APIs fluides des frameworks FestAssert ou ElasticSearch utilisés sur l’application, l’idée d’écrire des jeux de données en Java me plaisait bien. Et justement, il y’a quelques temps, l’argumentaire de l’article Why use DbSetup? ne m’avait pas laissé indifférent. C’était donc l’occasion d’utiliser cette jeune librairie développée par les français de Ninja Squad et qui mérite de se faire connaitre, j’ai nommé DbSetup.
Le guide utilisateur de DbSetup étant particulièrement bien conçu, l’objectif de cet article n’est pas de vous en faire une simple traduction, mais de vous donner envie de l’essayer et de vous présenter la manière dont je l’ai mis en oeuvre. Celle-ci s’éloigne en effet quelque peu de celle présentée dans la documentation, la faute à mes vieux réflexes d’utilisateur de DbUnit et au bienheureux rollback pattern de Spring.
Rendez autonomes vos Selenium
De nos jours, l’utilisation d’un serveur d’intégration continue pour déployer son application puis exécuter ses tests Selenium s’est relativement démocratisée. Néanmoins, l’investissement réalisé pour l’écriture de ces tests peut rapidement être mis à mal par le coût associé à leur maintenance. En effet, les tests d’IHM sont de nature plus instables que de simples tests unitaires. Outre des problématiques de rendu et de transversalité des fonctionnalités testées, l’une des principales difficultés réside dans la répétabilité des tests. Les données de test y jouent pour beaucoup. Cette difficulté est décuplée lorsque votre application repose sur une architecture SOA dont les services SOAP, XML ou bien REST sont hébergés par des tiers : vous n’avez aucune maitrise sur les données de l’environnement testé, ni sur sa stabilité.
Des tests qui échouent régulièrement à cause de données ayant été modifiées rendent laborieuse la détection de véritables régressions.
Cet article propose une solution appliquée depuis 2 ans sur une application de taille modeste (35 000 LOC pour 20 écrans). Continuer la lecture
Parallélisation de traitements batchs
Contexte
Récemment, j’ai participé au développement d’un batch capable d’indexer dans le moteur de recherche Elasticsearch des données provenant d’une base de données tierce. Développé en Java, ce batch s’appuie sur Spring Batch, le plus célèbre framework de traitements par lot de l’écosystème Java
Plus précisément, ce batch est décomposé en 2 jobs Spring Batch, très proches l’un de l’autre :
- le premier est capable d’initialiser à partir de zéro le moteur de recherche
- et le second traite uniquement les mouvements quotidiens de données.
Problématique
Au cours du traitement batch, l’exécution de la requête par Oracle pour préparer son curseur a été identifiée comme l’opération la plus couteuse, loin devant la lecture des enregistrements en streaming à travers le réseau, leur traitement chargé de construire les documents Lucene à indexer ou leur écriture en mode bulk dans ElasticSearch. A titre d’exemple, sur des volumétries de production, la préparation côté serveur Oracle d’une requête SQL ramenant 10 millions d’enregistrement peut mettre jusqu’à 1h30.
Avec pour objectif que le batch passe sous le seuil de 2h à moindre coût, 2 axes d’optimisations ont été étudiés : diminuer le temps d’exécution par Oracle et diminuer le temps de traitement.
Solutions étudiées
Les optimisations d’un DBA consistant à utiliser des tables temporaires et des procédures stockées n’ont pas été concluantes : trop peu de gains (10 à 20%) pour une réécriture partielle de notre batch, et avec le risque d’engendrer des régressions.
Après mesures et calculs, l’utilisation de la pagination sur des plages de 100, de 1 000 ou même de 10 000 enregistrements a également été écartée. Dans notre contexte, cela aurait dégradé les performances. Le choix de rester sur l’utilisation d’un curseur JDBC a été maintenu.
A cette occasion, nous avons remarqué que les temps de mise en place d’un curseur Oracle pour préparer 1 millions ou 10 millions d’enregistrements étaient du même ordre de grandeur.
Utilisant déjà l’une des techniques proposées par Spring Batch pour paralléliser notre traitement batch, pourquoi ne pas refaire appel à ses loyaux services ?
Spring Batch et ses techniques de parallélisation
Comme indiqué dans son manuel de référence, Spring Batch propose nativement 4 techniques pour paralléliser les traitements :
- Multi-threaded Step (single process)
- Parallel Steps (single process)
- Remote Chunking of Step (multi process)
- Partitioning a Step (single or multi process)
Pour optimiser le batch, 2 de ces techniques ont été utilisées.
Le Remote Chunking of Step a été écarté d’office. Dans le contexte client, installer un batch en production est déjà forte affaire. Alors en installer plusieurs interconnectés, je n’ose pas me l’imaginer : à étudier en dernier recours.
Le Multi-threaded Step est sans doute la technique la plus simple à mettre en œuvre. Seule un peu de configuration est suffisante : l’ajout d’un taskExecutor sur le tasklet à paralléliser. La conséquence majeure est que les items peuvent être traités dans le désordre.
Un prérequis à cette technique est que les ItemReader et ItemWiter soient stateless ou thread-safe. La classe de JdbcCursorItemReader de Spring Batch hérite de la classe AbstractItemCountingItemStreamItemReader qui n’est pas thread-safe. L’utilisation d’un wrapper synchronized aurait pu être envisagée si la classe fille de JdbcCursorItemReader développée pour les besoins du batch ne s’appuyait pas elle-même sur un RowMapper avec état reposant sur l’ordre de lecture des éléments.
Les Parallel Steps ont été mises en œuvre dès le début du batch pour traiter en parallèle des données de types différents (ex : Musique et Film). De par leurs jointures, les requêtes SQL de chacun différaient. Avant optimisation, 9 steps étaient déjà exécutés en parallèle par ce biais.
Quatrième et dernière technique, celle du Partitioning a Step est la piste que nous avons étudiée pour diminuer le temps d’exécuter des 3 steps les plus longs. Elle consiste à partitionner les données selon un critère pertinemment choisi (ex : identifiant, année, catégorie), le but étant d’obtenir des partitions de taille équivalente et donc de même temps de traitement.
Bien qu’il ne fut pas parfaitement linéairement réparti, le discriminant retenu pour le batch a été l’identifiant fonctionnel des données à indexer. Les données ont été découpées en 3 partitions. Comme attendu, bien que le volume de données soit divisé par trois, le temps de mise en place du curseur Oracle ne diminua pas. Par contre, le temps de traitements fut divisé par 3, faisant ainsi passer le temps d’exécution du batch de 3h à 2h.
Malgré une augmentation du nombre de requêtes exécutées simultanément, la base Oracle n’a pas montré de faiblesse. Une surcharge aurait en effet pu ternir ce résultat.
Exemple de mise en œuvre
Après ce long discours, rien de tel qu’un peu d’exercice. Pour les besoins de ce billet, et afin de capitaliser sur l’expérience acquise sur la configuration Spring Batch, j’ai mis à jour le projet spring-batch-toolkit hébergé sur GitHub. Le fichier blog-parallelisation.zip contient l’ensemble du code source mavenisé.
Je suis parti d’un cas d’exemple des plus simples : un batch chargé de lire en base de données des chefs-d’œuvre puis de les afficher sur la console.
En base, il existe 2 types de chefs-d’œuvre : les films et les albums de musique. Comme le montre le diagramme du modèle physique de données ci-contre, chaque type de chef-d’oeuvre dispose de sa propre table : respectivement MOVIE et MUSIC_ALBUM. Les données communes sont normalisées dans la table MAESTERPIECE.
En ce qui concerne le design du batch, le job peut être décomposé en 2 steps exécutés en parallèle, l’un chargé de traiter les albums de musique, l’autre les films. Une fois les 2 steps terminés, un dernier step affiche le nombre total de chefs-d’œuvre traités.
Avec une volumétrie de film supérieure à celle des albums, le step des films est décomposé en 2 partitions exécutées en parallèle.
Le besoin est simple. Partons d’une démarche TDD et commençons par l’écriture d’un test d’intégration.
Dans un premier temps, attaquons-nous aux données de test, sans doute ce qu’il y’a de plus fastidieux. Exécuté au moment de la création de la base de données embarquée, le script SQL TestParallelAndPartitioning.sql contient les ordres DDL du schéma ci-dessous ainsi que des requêtes INSERT permettant de l’alimenter.
Voici un exemple de données de tests :
Insert <strong>into</strong> MASTERPIECE (MASTERPIECE_ID, NAME, YEAR, GENRE) <strong>values</strong> (2, 'Star Wars: Episode IV - A New Hope!', 1977, 'Movie'); Insert <strong>into</strong> MOVIE (MOVIE_ID, MASTERPIECE_ID, REALISATOR, ACTORS) <strong>values</strong> (1, 2, 'George Lucas', 'Mark Hamill, Harrison Ford, Carrie Fisher'); … Insert <strong>into</strong> MASTERPIECE (MASTERPIECE_ID, NAME, YEAR, GENRE) <strong>values</strong> (4, 'The Wall', 1979, 'Music'); Insert <strong>into</strong> MUSIC_ALBUM (ALBUM_ID, MASTERPIECE_ID, BAND) <strong>values</strong> (3, 4, 'Pink Floyd');
Au total, 11 albums et 8 films sont référencés.
La classe de tests TestParallelAndPartitioning repose sur Spring Test, Spring Batch Test et JUnit.
Comme le montre l’extrait de code suivant, la classe JobLauncherTestUtils issue de Spring Batch Test permet d’exécuter notre unique job sans avoir à lui passer de paramètres ainsi que d’attendre la fin de son traitement.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestParallelAndPartitioning extends AbstractSpringBatchTest {
@Autowired
private JobLauncherTestUtils testUtils;
@Test
public void launchJob() throws Exception {
// Launch the parallelAndPartitioningJob
JobExecution execution = testUtils.launchJob();
// Batch Status
assertEquals(ExitStatus.COMPLETED, execution.getExitStatus());
// Movies
assertEquals("8 movies", 8, getStepExecution(execution, "stepLogMovie").getWriteCount());
// Music Albums
StepExecution stepExecutionMusic = getStepExecution(execution, "stepLogMusicAlbum");
assertEquals("11 music albums", 11, stepExecutionMusic.getWriteCount());
Object gridSize = ExecutionContextTestUtils.getValueFromStep(stepExecutionMusic,
"SimpleStepExecutionSplitter.GRID_SIZE");
assertEquals("stepLogMusicAlbum divided into 2 partitions", 2L, gridSize);
StepExecution stepExecPart0 = getStepExecution(execution,
"stepLogMusicAlbumPartition:partition0");
assertEquals("First partition processed 6 music albums", 6, stepExecPart0.getWriteCount());
StepExecution stepExecPart1 = getStepExecution(execution,
"stepLogMusicAlbumPartition:partition1");
assertEquals("Second partition processed 5 music albums", 5, stepExecPart1.getWriteCount());
}
L’exécution du job est suivie d’assertions :
- Le job s’est terminé avec succès
- Le step des films stepLogMovie a traité les 8 films attendus
- Le step des albums de musiques stepLogMusicAlbum a traité les 11 films attendus
- Et en y regardant de plus près, le step des albums a été décomposé en deux « sous-steps », stepLogMusicAlbumPartition:partition0 et stepLogMusicAlbumPartition:partition1 qui correspondent, comme leur nom l’indique, à chacune des 2 partitions. Les 11 films ont été séparés en 2 lots de capacités avoisinantes, à savoir de 6 et 5 films. Avec 3 partitions, on aurait pu s’attendre à un découpage de 4-4-3.
La configuration du batch commence par la déclaration de beans d’infrastructure Spring relativement génériques pour des tests :
- Une base de données en mémoire H2 initialisée avec le schéma des 6 tables de Spring Batch
- Le gestionnaire de transactions utilisé par Spring Batch pour gérer ses chunk
- Le JobRespository dans lequel seront persistés l’historique et le contexte d’exécution des batchs
- Les beans SimpleJobLauncher et JobLauncherTestUtils permettant d’exécuter le job testé
Ces beans sont déclarés dans le fichier AbstractSpringBatchTest-context.xml :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
">
<!-- Create an in-memory Spring Batch database from the schema included into the spring-batch-core module -->
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:org/springframework/batch/core/schema-drop-h2.sql" />
<jdbc:script location="classpath:org/springframework/batch/core/schema-h2.sql" />
</jdbc:embedded-database>
<!-- Datasource transaction manager used for the Spring Batch Repository and batch processing -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
<!-- Helps with testing (autowired, injected in the test instance) -->
<bean class="org.springframework.batch.test.JobLauncherTestUtils" lazy-init="true" />
<!-- Starts a job execution -->
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
<!-- In-memory database repository -->
<batch:job-repository id="jobRepository" />
</beans>
La majeure partie de la configuration Spring est définie dans le fichier TestParallelAndPartitioning-context.xml d’où sont tirés les extraits suivants.
En plus du schéma nécessaire par le JobRepository persistant de Spring Batch, les 3 tables de notre exemple sont créées puis alimentées avec notre jeu de données comportant 19 chefs-d’œuvre :
<!-- Initialize database with 8 movies and 11 music albums -->
<jdbc:initialize-database>
<jdbc:script location="classpath:com/javaetmoi/core/batch/test/TestParallelAndPartitioning.sql" />
</jdbc:initialize-database>
Un pool de threads sera utilisé pour paralléliser le job . Ce pool est dimensionné à 4 threads : un thread pour chacun des 2 parallel steps + un thread pour chacun des 2 « sous-steps » correspondants aux 2 partitions.
<!-- Thread pools : 1 thread for stepLogMovie and 3 threads for stepLogMusicAlbum -->
<task:executor id="batchTaskExecutor" pool-size="4" />
Vient ensuite la déclaration du job Spring Batch. L’utilisation des balises split et flow permet de mettre en œuvre les Parallel Steps. Couplée avec l’attribut task-executor, l’enchainement des Steps référencés par les flows n’est alors plus linéaire.
Les 2 flows flowMovie et flowMusicAlbum sont exécutés en parallèle. Une fois ces 2 flows terminés, le step stepEnd terminera le job.
<!-- Job combining both parallel steps and an local partitions -->
<batch:job id="parallelAndPartitioningJob">
<batch:split id="splitIndexDelta" task-executor="batchTaskExecutor" next="stepEnd">
<!-- 2 parall steps. The first one will be partitioned -->
<batch:flow parent="flowMusicAlbum" />
<batch:flow parent="flowMovie" />
</batch:split>
<!-- The stepEnd will be executed after the 2 flows flowMusicAlbum and flowMovie -->
<batch:step id="stepEnd">
<batch:tasklet>
<bean class="com.javaetmoi.core.batch.test.EndTasklet" />
</batch:tasklet>
</batch:step>
</batch:job>
Composé d’un seul step (sans partition), la déclaration du flow flowMusicAlbum chargée de logger les films est la plus simple. De type chunk, le step a un reader utilisant un curseur JDBC pour itérer sur la liste des films. La classe BeanPropertyRowMapper permet d’effectuer le mapping entre les colonnes du ResultSet de la requête SQL et le bean java Movie ; il se base sur le nom des colonnes et le nom des propriétés du bean.
Le writer affiche les propriétés du bean Movie à l’aide de la méthode ToStringBuilder.reflectionToString() d’Apache Commons Lang.
L’attribut commit-interval du chunk est fixé volontairement à 2. Ainsi, le writer est appelé tous les 2 films. Cela permet de voir plus facilement l’enchevêtrement des différents threads.
<!-- The movie flow is composed of a single step that reads all movies then log them -->
<batch:flow id="flowMovie">
<batch:step id="stepLogMovie">
<batch:tasklet>
<batch:chunk writer="anyObjectWriter" commit-interval="2">
<batch:reader>
<bean class="org.springframework.batch.item.database.JdbcCursorItemReader" p:dataSource-ref="dataSource">
<property name="rowMapper">
<bean class="org.springframework.jdbc.core.BeanPropertyRowMapper" c:mappedClass="com.javaetmoi.core.batch.test.Movie" />
</property>
<property name="sql">
<value><![CDATA[
select a.masterpiece_id as id, name, year, realisator, actors
from masterpiece a
inner join movie b on a.masterpiece_id=b.masterpiece_id
where genre='Movie'
]]></value>
</property>
</bean>
</batch:reader>
</batch:chunk>
</batch:tasklet>
</batch:step>
</batch:flow>
Le flow chargé de traiter les films est lui aussi composé d’un seul step : stepLogMusicAlbum. Ce dernier est partitionné en 2 (propriété grid-size= »2″ du handler). Le même pool de threads est utilisé pour traiter les 2 partitions. Le bean chargé de partitionner les données est référencé : partitionerMusicAlbum. Le traitement des « sous-steps » partitionnés est confié au bean stepLogMusicAlbumPartition.
<!-- The music flow is composed of a single step which is partitioned -->
<batch:flow id="flowMusicAlbum">
<batch:step id="stepLogMusicAlbum">
<!-- Executes partition steps locally in separate threads of execution -->
<batch:partition step="stepLogMusicAlbumPartition" partitioner="partitionerMusicAlbum">
<batch:handler grid-size="2" task-executor="batchTaskExecutor" />
</batch:partition>
</batch:step>
</batch:flow>
Le bean partitionerMusicAlbum repose sur la classe ColumnRangePartitioner reprise des samples Spring Batch La clé de partition doit lui être précisé sous forme du couple nom de table / nom de colonne.
Techniquement, cette classe utilise ces données pour récupérer les valeurs minimales et maximales de la clé. Pour se faire, 2 requêtes SQL sont exécutées. A partir, du min et du max, connaissant le nombre de partitions à créer (grid-size), elle calcule des intervalles de données de grandeur équivalente. Afin que les partitions soient de taille équivalente en termes de données, les valeurs des clés doivent être uniformément distribuées. C’est par exemple le cas avec un identifiant technique généré par une séquence base de données et pour lesquelles aucune donnée n’est supprimée (pas de trou). Les clés minValue et maxValue de chaque intervalle sont mises à disposition dans le contexte d’exécution de chaque « sous-step ».
<!-- The partitioner finds the minimum and maximum primary keys in the music album table to obtain a count of rows and
then calculates the number of rows in the partition -->
<bean id="partitionerMusicAlbum" class="com.javaetmoi.core.batch.partition.ColumnRangePartitioner">
<property name="dataSource" ref="dataSource" />
<property name="table" value="music_album" />
<property name="column" value="album_id" />
</bean>
De la même manière que son cousin stepLogMovie, le bean stepLogMusicAlbumPartition est composé d’un chunk tasklet. Celui-ci référence 2 beans définis dans la suite du fichier de configuration : readerMusicAlbum et anyObjectWriter, ce dernier étant déjà utilisé par le bean stepLogMovie.
<!-- Read music albums from database then write them into logs -->
<batch:step id="stepLogMusicAlbumPartition">
<batch:tasklet>
<batch:chunk reader="readerMusicAlbum" writer="anyObjectWriter" commit-interval="2" />
</batch:tasklet>
</batch:step>
Par rapport à celui en charge de la lecture des films, le bean readerMusicAlbum se démarque en 2 points :
- La requête SQL filtre non seulement les chefs-d’œuvre par leur genre (where genre=’Music’), mais également sur une plage d’identifiants (and b.album_id >= ? and b.album_id <= ?) relatifs à la clé de partitionnement. Cette requête est donc dynamique. Basé sur un PreparedStatement JDBC, elle est exécutée autant de fois qu’il y’a de partitions à traiter.
Les 2 paramètres de la requête (symbolisés par un ?) sont évalués dynamiquement à partir du contexte d’exécution du step. Une Spring Expression Language (SPeL) est utilisée dans la définition du bean anonyme basé sur la classe ListPreparedStatementSetter. Ceci est permis grâce à la portée du bean reader qui est de type step (scope= »step »).
<!-- JdbcCursorItemReader in charge of selecting music albums by id range -->
<bean id="readerMusicAlbum" class="org.springframework.batch.item.database.JdbcCursorItemReader" scope="step"
p:dataSource-ref="dataSource">
<property name="rowMapper">
<bean class="org.springframework.jdbc.core.BeanPropertyRowMapper" c:mappedClass="com.javaetmoi.core.batch.test.MusicAlbum" />
</property>
<property name="sql">
<value><![CDATA[
select a.masterpiece_id as id, name, year, band
from masterpiece a
inner join music_album b on a.masterpiece_id=b.masterpiece_id
where genre='Music'
and b.album_id >= ? and b.album_id <= ?
]]></value>
</property>
<property name="preparedStatementSetter">
<bean class="org.springframework.batch.core.resource.ListPreparedStatementSetter">
<property name="parameters">
<list>
<!-- SPeL parameters order is important because it referes to "where album_id >= ? and album_id <= ?" -->
<value>#{stepExecutionContext[minValue]}</value>
<value>#{stepExecutionContext[maxValue]}</value>
</list>
</property>
</bean>
</property>
</bean>
Après épuration des logs et ajout d’un Thread.sleep(50) dans la classe ConsoleItemWriter dans le but, voici le résultat de l’exécution du batch :
Job: [FlowJob: [name=parallelAndPartitioningJob]] launched with the following parameters: [{timestamp=1354297881856}] Executing step: [stepLogMusicAlbum] Executing step: [stepLogMovie] Movie[realisator=George Lucas,actors=Mark Hamill, Harrison Ford, Carrie Fisher,id=2,name=Star Wars: Episode IV - A New Hope!,year=1977] Movie[realisator=Richard Marquand,actors=Mark Hamill, Harrison Ford, Carrie Fisher,id=6,name=Star Wars : Episode VI - Return of the Jedi,year=1983] Movie[realisator=Paul Verhoeven,actors=Arnold Schwarzenegger, Sharon Stone,id=7,name=Total Recal,year=1990] Movie[realisator=James Cameron,actors=Arnold Schwarzenegger,id=11,name=Terminator 2 : Judgement Day,year=1991] MusicAlbum[band=The Beatles,id=1,name=Help!,year=1965] MusicAlbum[band=The Police,id=3,name=Outlandos d'Amour!,year=1978] MusicAlbum[band=Metallica,id=10,name=Black Album,year=1991] MusicAlbum[band=Radiohead,id=13,name=OK Computer,year=1997] Movie[realisator=Quentin Tarantino,actors=John Travolta, Samuel L. Jackson, Uma Thurman,id=12,name=Pulp Fiction,year=1994] Movie[realisator=Peter Jackson,actors=Elijah Wood, Sean Astin,id=15,name=The Lord of the Rings: The Return of the King,year=2003] MusicAlbum[band=Pink Floyd,id=4,name=The Wall,year=1979] MusicAlbum[band=U2,id=5,name=War,year=1983] MusicAlbum[band=Muse,id=14,name=Showbiz,year=1999] MusicAlbum[band=Muse,id=16,name=The Resistance,year=2009] Movie[realisator=Christopher Nolan,actors=Leonardo DiCaprio, Marion Cotillard,id=17,name=Inception,year=2010] Movie[realisator=Christopher Nolan,actors=Christian Bale, Gary Oldman,id=18,name=The Dark Knight Rises,year=2012] MusicAlbum[band=U2,id=8,name=Achtung Baby,year=1991] MusicAlbum[band=Nirvana,id=9,name=Nevermind,year=1991] MusicAlbum[band=Saez,id=19,name=Messina,year=2012] Executing step: [stepEnd] 19 masterpiece(s) have been processed Job: [FlowJob: [name=parallelAndPartitioningJob]] completed with the following parameters: [{timestamp=1354297881856}] and the following status: [COMPLETED]
Ces traces confirment que le traitement des chefs-d’œuvre est équitablement réparti dans le temps et entre les différents threads, avec une alternance de films et d’albums de musique, et des albums des 2 partitions traités en parallèle.
Conclusion
Pour un effort minime, à peine quelques heures de développement, la durée d’exécution du batch a baissé de 33%, avec un débit avoisinant les 5 000 documents par secondes indexés dans ElasticSearch. Pourquoi donc s’en priver ?
La documentation Spring Batch doit être attentivement suivie pour ne pas tomber dans certains pièges liés à la parallélisassion. La documentation officielle, le livre Spring Batch in Action et maintenant ce billet devraient être des sources suffisantes pour comprendre et mettre en œuvre aux moins 2 des techniques proposées nativement par Spring Batch : Parallel Steps et Partitioning a Step.
Générer des tests JMeter à partir de Selenium
Chez mon client, des tests de stress sont réalisés sur toute nouvelle version d’une application. Outre le fait de qualifier techniquement l’environnement de pré-production, ces tirs permettent de détecter toute dégradation des performances et de prévenir toute montée en charge induite, par exemple, par une nouvelle fonctionnalité. Plus encore, ils permettent de mesurer les gains apportés par d’éventuelles optimisations. Ces tests de stress sont réalisés à l’aide de l’outil Apache JMeter [1].
Afin de pouvoir comparer des mesures, les cas fonctionnels utilisés lors des tests doivent, dans la mesure du possible, être identiques aux précédents tirs, sachant que ces derniers peuvent dater de plusieurs mois. Entre temps, nombre d’évolutions ont été susceptibles de casser vos tests JMeter. A priori, vous avez donc 2 choix : soit vous les réécrivez, soit vous les maintenez à jour. Si vous en avez déjà écrit, vous vous doutez bien que maintenir dans la durée des tests JMeter a un cout non négligeable. Une 3ième solution présentée ici consiste à la générer !
J’ai la chance de travailler dans une équipe ou l’outil Selenium [2] de tests IHM est rentré dans les mœurs. L’automatisation de leur exécution y joue un rôle indéniable. Notre hiérarchie s’est fortement impliquée ; elle a investi de l’énergie et du budget. Un DSL a été mis au point pour faciliter leur écriture et leur maintenance. Alors quand on peut les rentabiliser encore davantage, autant le faire. J’ai donc proposé de ne maintenir que les tests Selenium et de générer les tests JMeter à partir de tests Selenium.
Cet article a pour objectif de vous présenter la démarche adoptée. Si vous êtes intéressés, vous pourrez librement l’adapter en fonction de votre contexte projet. Continuer la lecture