Metrics, Pour Mesurer Efficacement Les Performances : Les Bases

présentation de Metrics

Je vous propose dans cette série de 4 articles, de vous présenter la librairie Metrics,
initié par la société Yammer.
Celle-ci permet de fournir des métriques au niveau applicatif et JVM.

Ce premier article, présente les différents types de mesures disponibles dans Metrics, leur usage et leur installation.

pourquoi mesurer ?

A l’instar des grands du web, il devient primordial de mesurer les choses avant d’agir.
Cela permet de matérialiser, identifier et comprendre le fonctionnement interne en production de nos applications. Cela permet aussi de lever des freins techniques ou fonctionnels, notamment en comprenant la valeur métier de certaines fonctionnalités, les dysfonctionnements techniques ou le manque de performances. Pour cela, rien de tel qu’une mesure fiable, numérique, pour comprendre, et prendre des décisions.

La librairie Metrics, est une bonne solution pour répondre à ce besoin.

installation de metrics

Pour installer Metrics, il faut rajouter à son fichier maven pom.xml, la section suivante :

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>

montée de version, renommage de package

A noter qu’à partir de la version 3, Metrics a pour package de base com.codahale.metrics, et non com.yammer.metrics (cas des versions 2.x).

Isolation des métriques par application

Toutes les métriques de Metrics, sont stockées dans une instance de MetricsRegistry. Pour que plusieurs applications au sein de la même JVM, aient des métriques dissociées, il faut que chacune d’elles créent leur propre instance (chaque application étant isolée car possédant son propre classloader). Ceci peut soit se faire via le framework d’injection de dépendances (Spring, Guice), soit via un servletListener pour des applications JEE, ou via un champ statique public final de chaque application, accessible depuis l’extérieur de la classe.

1
public static final MetricRegistry metrics = new MetricRegistry();

les métriques disponibles

Le registre installé, nous pouvons créer les métriques dont nous avons besoin.

Voici les différents types de métriques disponibles :

  • la jauge
  • le compteur
  • la mesure
  • l’histogramme
  • la mesure
  • le timer

Lors de cette création, nous pouvons les identifier de façon unique dans le registre par les éléments suivants :

  • groupe : la valeur par défaut est le package de la classe
  • type : nom de classe
  • nom : décrit le but de la métrique
  • scope (optionel) : permet de différentier des métriques de plusieurs instances d’une même classe

la jauge

La jauge représente la valeur instantanée de ce qui est mesuré. Cette mesure simple, est à utiliser quand nous ne maîtrisons pas directement son incrément et son décrément.
C’est le cas quand la valeur représente un système externe, ou quand celle-ci est maîtrisée par une librairie tierce.

Un exemple d’usage de la jauge est le nombre de sessions actives dans une application web.

1
2
3
4
5
6
registry.register(name(SessionStore.class, "active-sessions"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return store.getActiveSessions();
}
});

la jauge JMX

Cette jauge spécialisée permet d’intégrer facilement dans le registre Metrics, une valeur exposée via JMX. Metrics peut donc devenir l’unique référentiel de métriques applicatives, en récupérant des données tierces via JMX, afin de les exposer via d’autres canaux (JMX, Graphite, Ganglia etc…).

1
2
registry.register(name(SessionStore.class, "cache-evictions"),
new JmxAttributeGauge("net.sf.ehcache:type=Cache,scope=sessions,name=eviction-count", "Value"));

la jauge ratio

Cette jauge permet d’intégrer dans Metrics, le résultat du rapport entre deux variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CacheHitRatio extends RatioGauge {
private final Meter hits;
private final Timer calls;
public CacheHitRatio(Meter hits, Timer calls) {
this.hits = hits;
this.calls = calls;
}
@Override
public Ratio getValue() {
return Ratio.of(hits.oneMinuteRate(),
calls.oneMinuteRate());
}
}

la jauge cachée

Cette jauge permet de réutiliser pendant un temps paramétrable via un cache, un résultat coûteux à récupérer.

1
2
3
4
5
6
7
8
registry.register(name(Cache.class, cache.getName(), "size"),
new CachedGauge<Long>(10, TimeUnit.MINUTES) {
@Override
protected Long loadValue() {
// assume this does something which takes a long time
return cache.getSize();
}
});

la jauge dérivée

Cette jauge permet de calculer un résultat à partir d’une jauge déjà présente dans le registre de Metrics.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class QueueManager {
private final Queue queue;
public QueueManager(MetricRegistry metrics, String name) {
this.queue = new Queue();
metrics.register(MetricRegistry.name(QueueManager.class, name, "size"),
new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
});
}
}

le compteur

représente une mesure maîtrisée par l’application, qui s’incrémente et se décrémente.
Un exemple de compteur pourrait être le nombre de tâches planifiées en cours d’exécution à un instant t.
Tous les compteurs ont une valeur initiale de 0.

1
2
3
4
5
6
7
8
9
10
11
private final Counter pendingJobs = metrics.counter(name(QueueManager.class, "pending-jobs"));
public void addJob(Job job) {
pendingJobs.inc();
queue.offer(job);
}
public Job takeJob() {
pendingJobs.dec();
return queue.take();
}

l’histogramme

L’histogramme représente la distribution statistique de valeurs d’un ensemble de données.
Cela nous permet d’avoir des informations comme la moyenne, le minimum, le maximum, la médiane, la déviation standard, les quartiles ou percentiles.
Afin d’éviter un plantage, lorsque le calcul porte sur un grand nombre de données, Metrics réalise un échantillonage statistique représentatif, via différents algorithmes, appelés Reservoirs.

Les réservoirs fournis, hormis le dernier présenté, ont la particularité d’avoir une occupation mémoire restreinte.

Des échantillons uniformes

Il existe l’algorithme de Vitter, qui produit des échantillons uniformes. Celui-ci est intéressant pour faire une analyse à long terme, mais n’est pas adaptée pour savoir si la distribution des données porte un changement significatif récent.

1
private final static Histogram histogram= METRIC_REGISTRY.register("myLongTermHistogram",new Histogram(new UniformReservoir()));

Des échantillons récents

Il existe aussi un algorithme mettant un fort poids statistique sur des données récentes. Celui-ci est à privilégier lorsque l’on veut avoir une vision récente de la distribution des évenements.

Cette algorithme est utilisé par défaut dans les Timers.

1
private final static Histogram histogram= METRIC_REGISTRY.register("myshortTermHistogram",new Histogram(new ExponentiallyDecayingReservoir()));

Des échantillons à fenêtre glissante

cet algorithme permet d’étudier la distribution des N dernières données.

1
private final static Histogram histogram= METRIC_REGISTRY.register("",new Histogram(new SlidingWindowReservoir(1024)));

Des échantillons temporels à fenêtre glissante

cet algorithme permet d’étudier la distribution des N dernières secondes.
Un bémol est à apporter concernant cet algorithme, qui contrairement aux autres, porte sur la totalité des données des N dernières secondes, ce qui peut représenter un volume en mémoire important. Cet algorithme est aussi le plus lent.

1
private final static Histogram histogram= METRIC_REGISTRY.register("lastTenMinutes",new Histogram(new SlidingTimeWindowReservoir(10,TimeUnit.MINUTES)));

la mesure

La mesure mesure la fréquence à laquelle surviennent les évenements sur les périodes suivantes :

  • la durée de vie complète de l’application
  • les 15 dernières minutes
  • les 5 dernières minutes
  • la dernière minute

Le nombre de requêtes par seconde pendant les 5 dernières minutes est un bon exemple de mesures.

L’alimentation de cette mesure se fait de la façon suivante :

1
2
3
4
5
private final Meter requests = metrics.meter(name(RequestHandler.class, "requests"));
public void handleRequest(Request request, Response response) {
requests.mark();
}

le timer

Le timer représente un histogramme des durées et une mesure de la fréquence d’apparition.

Un résulat issu de l’usage du timer serait :

à 1500 requêtes par seconde, notre latence augmente de 25 à 357 ms.

1
2
3
4
5
6
7
8
final Timer timer = registry.timer(name(Filter.class, "requests"));
final Timer.Context context = timer.time();
try {
// handle request
} finally {
context.stop();
}

Conclusion

J’espère que ce premier article sur Metrics, vous permettra d’avoir une vue d’ensemble des différentes métriques disponibles.

Les trois autres articles de cette série porteront, dans l’ordre, sur l’intégration de Metrics dans une application web JEE, l’intégration de Metrics avec spring et guice et l’intégration de Metrics avec JDBC, logback et jersey.

Références

Web-Debug-Tag : Visualiser Facilement Les Variables Des Jsp Dans Son Navigateur

Un besoin de communication

Lors de mes missions, j’ai pu observer différentes organisations d’équipe de développement d’applications web.

Dans certains cas, une sous-équipe est dédiée uniquement à la réalisation de la couche graphique (le “front”), et une autre à la partie serveur (le “back”). Ceci peut engendrer une “frontière” technique, une opacité quant aux variables disponibles dans les JSP fournies par les développeurs “back”.

Afin d’améliorer la communication entre les équipes, Jérémy Goupil et moi avons codé une taglib appelée web-debug-tag, permettant de rendre visible aux développeurs front ces variables.

Je vous présente donc dans cet article, son principe, son installation, et son usage, d’abord dans une application web simple, puis dans un cas plus complexe.

Web-debug-tag, kesako ?

Cette taglib sérialise dans la réponse du serveur, au format JSON, les variables et leurs valeurs, qu’elles soient dans les scopes page, request, session ou application.

Les valeurs des variables peuvent être aussi bien des chaînes de caractères que des arbres d’objets, le tag utilisant la librairie Jackson pour sérialiser le tout. Seuls les champs publics des objets, ou ceux ayant des accesseurs publics seront sérialisés.

De plus, une fois ces objets java sérialisés en JSON, du code client met dans la console javascript ce résultat pour être facilement visualisé dans le navigateur.

Installation

L’installation de cette librairie, pour les projets sous maven, nécessite l’ajout dans votre fichier pom.xml de la section suivante :

<dependency>
    <groupId>fr.figarocms</group>
    <artifactId>web-debug-tag</artifactId>
    <version>1.6</version>
</dependency>

Cet ajout permettra à votre projet d’inclure dans votre fichier war, le jar de la taglib.

Une fois installé, vous pouvez maintenant inclure dans vos JSP la directive suivante:

<%@ taglib prefix="debug" uri="https://github.com/figarocms/web-debug-tag"%>

suivi par le tag en lui-même:

<debug:debugModel/>

SI votre application comporte de nombreuses pages avec des parties communes, vous utilisez certainement un moteur de template. Placez donc cette directive dans un template global pour qu’il s’applique à toutes les JSP de l’application.

À utiliser uniquement pour le développement

Cette librairie exposant toutes les variables de l’application côté navigateur, celle-ci n’est pas activée par défaut ; {“l’activation du web-debug-tag en production est totalement déconseillée”}, principalement pour des raisons de sécurité.

Pour l’activer, il faut donc mettre dans la ligne de commande lançant votre serveur d’applications le paramètre suivant:

-Ddebug.jsp=true

Un message d’avertissement s’affichera néanmoins à chaque démarrage pour vous rappeler que ce n’est qu’une librairie dédiée aux environnements de développement.

Une application web d’exemple

J’ai codé une application basique afin d’illustrer l’usage du web-debug-tag.

Pour tester le résultat, vous pouvez cloner le repository github par la commande suivante :

git clone https://github.com/clescot/web-debug-tag-example

Dans le répertoire web-debug-tag-example nouvellement crée, vous pouvez lancer la commande maven suivante pour lancer un serveur jetty:

mvn org.eclipse.jetty:jetty-maven-plugin:9.0.2.v20130417:run-exploded

Rendu

Le tag génère du code, qui met dans l’objet javascript console la grappe d’objets JSON générée.

voici un exemple de rendu sous ce même jetty:

{% img right /images/capture_web-debug-tag.png 1855 1056 les variables jsp apparaissent dans l'onglet console de firebug %}

On peut voir dans cette capture d’écran, la sérialisation d’éléments présents dans les différents scopes de l’application web créée pour l’article, via la console Firebug sous Firefox. D’autres navigateurs tels Chrome permettent de visualiser aussi la sortie de l’objet console.

On pourra noter dans cette capture, la sérialisation d’objets dans les différents scopes, que les objets soient simples (chaînes de caractères, entiers), ou complexes (objets contenus dans d’autres objets).

Exclusion de certaines classes non sérialisables

Certaines classes particulières, ne sont pas sérialisables comme des proxies Spring, Sitemesh ou Hibernate, sauf à utiliser des modules Jackson tierces. Ceci se traduit par une JsonMappingException levée par Jackson, qui englobe des StackOverflowError, syndrôme de la boucle infinie.

Il est donc possible, via l’utilisation d’un paramètre de contexte dans le fichier web.xml, de définir les classes à exclure de la sérialisation via une expression régulière. A noter que ces classes peuvent être présentes à différents niveaux de la grappe d’objets à sérialiser.

voici un exemple de configuration d’exclusion évitant des problèmes liés aux classes de Tomcat 7, Jetty 9, sitemesh et Spring:

<context-param>
    <param-name>webdebug.excludes</param-name>
    <param-value>org.springframework.*,__spring.*,__sitemesh.*,org.apache.jasper.*,org.apache.catalina.*,org.eclipse.jetty.webapp.Context,org.eclipse.jetty.server.*,org.eclipse.jetty.servlet.*,org.eclipse.jetty.webapp.*</param-value>
</context-param>

Utilisation du web-debug-tag dans une application web plus complexe

l’équipe du framework Spring, fournit une application web exemple à laquelle j’ai ajouté, dans un fork du repository github, le web-debug-tag, pour tester une situation plus complexe.

voici sa configuration dans le web.xml, créée après plusieurs essais permettant d’éviter des JsonMappingException :

 <context-param>
    <param-name>webdebug.excludes</param-name>
    <param-value>org.springframework.web.context.support.ServletContextResourcePatternResolver,org.apache.naming.*,
        org.springframework.aop.*,net.sf.ehcache.DiskStorePathManager,org.springframework.context.support.*,__spring.*,__sitemesh.*,org.apache.jasper.*,org.apache.catalina.*,org.eclipse.jetty.webapp.Context,net.sf.ehcache.CacheManager,org.apache.commons.dbcp.BasicDataSource,org.springframework.beans.factory.support.*,org.eclipse.jetty.server.*,org.eclipse.jetty.servlet.*,org.eclipse.jetty.webapp.*</param-value>
</context-param>

Cette configuration pourrait être affinée si nécessaire, afin de voir plus d’informations liées à Spring (les développeurs web en ont-ils besoin ?).

voici un exemple de rendu des variables via le web-debug-tag :

onglet console de firebug

conclusion

Je vous ai présenté l’installation et l’usage du web-debug-tag. Cet outil a eu un retour positif des développeurs “front” chez mon client.

J’espère que ce tag vous sera utile, permettant une meilleure communication entre les parties “front” et “back”, si vos équipes sont structurées ainsi.

Exécuter en Parallèle Ses Tests Junit Avec Tempus-Fugit

… ou comment tester si son code est thread-safe.

Le problème

Lors du développement de services exposés sur internet (services web, servlets etc… ), il est primordial de s’assurer de la nature thread-safe du code.

Des problèmes d’écriture et lecture de données concurrentes, provoquant des erreurs difficilement reproductibles peuvent
survenir : un service sous forte charge pourrait se comporter de façon erronée sous forte charge. en bref, le code est hors de contrôle.

Une solution

L’approche habituelle pour détecter cette faiblesse est de favoriser la reproduction du problème par l’exécution concurrente (via plusieurs threads) et répétée du code.

Concurrence et répétition via Tempus-fugit

Je vous propose d’exécuter en parallèle le code via des tests unitaires [Junit] et la librairie [tempus-fugit].

Nous testerons pour cet article, la nature thread-safe d’un service REST développé avec Jersey. Ces tests mettront en oeuvre les annotations @Concurrent, Repeating, @Intermittent et le runner ConcurrentTestRunner.

Les exemples de code sont présent sur un repository github dédié.

Un exemple de service web

Voici un service web [JAX-RS], qui présente deux points d’entrée permettant :

  • d’ajouter un élément à une collection
  • d’effacer le contenu de la collection

Le code ci-dessous, a pour seul but d’illustrer les problèmes de concurrence d’accès. Ici, le problème vient de l’utilisation du champ de classe coll, sans synchronisation. L’accès à un champ de la classe, pour des services web, est la plupart du temps la partie critique du code à contrôler, que ce champ soit un champ d’instance ou un champ statique. Ainsi, il est nécessaire de garantir un accès unique au champ via du code synchronisé, ou de déclarer volatile le champ pour éviter que java ne cache sa valeur pour chaque thread.
En bref, “utilisez le moins possible de champs d’instance pour des services web.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Path("/test1")
public class FooBar {
private static Collection coll = Lists.newArrayList();
@GET
@Path("/path1")
public Response test(@QueryParam("value") String value) throws InterruptedException {
coll.add(value);
int size = coll.size();
return Response.ok(""+size).build();
}
@GET
@Path("/clear")
public Response test2(@QueryParam("value") String value){
coll.clear();
return Response.ok("ok").build();
}
}

L’exécution parallèle

Comme indiqué précédemment, l’exécution parallèle est une des contraintes à imposer au code pour favoriser l’émergeance du problème.

La librairie tempus-fugit nous propose deux mécanismes pour exécuter du code en parallèle :

  • l’annotation @Concurrent
  • le runner ConcurrentTestRunner

L’annotation @Concurrent

La librairie [Tempus-Fugit], fournit une TestRule , intitulée ConcurrentRule, qui s’active lorsque l’annotation @Concurrent est présente.

"L'annotation @Concurrent permet d'exécuter une même méthode de test de façon concurrente sur plusieurs threads."

A noter que les TestRule étaient anciennement appelées MethodRule jusqu’à [Junit] 4.8.

Les deux tests ci-dessous permettent de voir l’intérêt de cette TestRule :

  • le premier test ne contient pas l’annotation @Concurrent. Il n’est donc exécuté qu’une seule fois, et s’exécute avec succès. Le non support d’une exécution concurrente n’est donc pas détecté.
  • le second test est identique au premier, mais contient l’annotation @Concurrent. Ce Test est exécuté deux fois(@concurrent(count=2), via deux threads distincts. Ce second test permet de détecter le non-support du code d’une exécution multi-threadée, notamment à cause de l’utilisation du champ de classe coll.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class MultiThreadTest extends WebRunner {
private static Logger LOGGER = LoggerFactory.getLogger(MultiThreadTest.class);
@Rule
public ConcurrentRule concurrentRule = new ConcurrentRule();
private WebResource webResource1 = resource().path("/test1/path1").queryParam("value", "4");
@Test
public void testResources_without_concurrent_annotation() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}
@Test
@Concurrent(count = 2)
public void testResources_with_concurrent_annotation() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}
private String call(final WebResource webResource) {
String response = null;
try {
response = webResource.get(String.class);
assertThat("path =" + webResource.getURI() + "response=" + response, !response.isEmpty(), is(true));
LOGGER.info("response=" + response);
} catch (UniformInterfaceException t) {
Assert.fail(t.getMessage());
LOGGER.error("erreur message=" + t.getMessage());
LOGGER.error("erreur réponse=" + t.getResponse());
}
return response;
}
}

Le runner ConcurrentTestRunner

A l’instar de l’annotation @Concurrent, un runner Junit permet d’exécuter l’ensemble des tests d’une classe chacun dans un Thread.
Ainsi, la classe de test suivante comporte 3 méthodes de test : 3 threads seront donc crées pour exécuter dans chacun d’eux une méthode de test en parallèle.

Le léger avantage de cette voie est la simplicité : il sufit de déclarer le runner pour que toutes les méthodes soient exécutés en parallèle ; pas besoin de déclarer la TestRule et d’annoter chaque test.

Les deux principaux inconvénients sont le manque de flexibilité car on ne peut spécifier qu’un seul runner Junit pour une classe de test, et le nombre de threads par méthode de test est fixe (1 thread par test).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@RunWith(ConcurrentTestRunner.class)
public class ConcurrentTest extends WebRunner{
private static Logger LOGGER = LoggerFactory.getLogger(MultiThreadTest.class);
private WebResource webResource1 = resource().path("/test1/path1").queryParam("value", "4");
@Test
public void first_test() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}
@Test
public void second_test() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}
@Test
public void third_test() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}
private String call(final WebResource webResource) {
String response = null;
try {
response = webResource.get(String.class);
assertThat("path =" + webResource.getURI() + "response=" + response, !response.isEmpty(), is(true));
LOGGER.info("response=" + response);
} catch (UniformInterfaceException t) {
Assert.fail(t.getMessage());
LOGGER.error("erreur message=" + t.getMessage());
LOGGER.error("erreur réponse=" + t.getResponse());
}
return response;
}
}

L’exécution répétée

La seconde contrainte à imposer au code est l’exécution répétée.

La librairie tempus-fugit nous propose deux mécanismes pour exécuter du code de façon répétée :

  • l’annotation @Repeating
  • L’annotation @Intermittent

L’annotation @Repeating

Souvent, la résolution des problèmes d’exécution concurrente de code n’est pas évidente, contrairement à l’exemple de cet article. Le bug se produit de façon erratique. Il est donc nécessaire de reproduire l’exécution des tests un grand nombre de fois, pour avoir la chance de faire échouer le code.

Tempus-fugit, nous fournit une autre TestRule intitulée RepeatingRule, qui est associée avec l’annotation @Repeating.

Elle permet d’exécuter un grand nombre de fois une méthode de test. Elle peut se cumuler avec l’annotation @Concurrent.

Voici un exemple d’une utilisation combinée de ces deux annotations :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Rule
public RepeatingRule repeatingRule = new RepeatingRule();
@Test
@Concurrent(count = 2)
@Repeating(repetition=4)
public void testResources_with_concurrent_annotation() {
for (long size = 1; size < 10; size++) {
assertThat(Long.parseLong(call(webResource1)), is(size));
}
WebResource webResource2 = resource().path("/test1/clear");
call(webResource2);
}

L’annotation @Intermittent

Cette annotation a de grandes similitudes avec l’annotation @Repeating, à ceci prêt qu’elle n’est pas associée avec une TestRule, mais avec le runner Junit IntermittentTestRunner. Elle possède également un attribut nommé repetition.

Cette association a pour avantage d’exécuter à chaque répétition, les méthodes annotées par @Before et @After, contrairement aux TestRule.
{“Le principal inconvénient de @Intermittent est qu’on ne peut utiliser plus d’un runner Junit”}; on ne peut donc pas dans cette configuration utiliser un autre runner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RunWith(IntermittentTestRunner.class)
public class IntermittentTest extends WebRunner{
private static Logger LOGGER = LoggerFactory.getLogger(MultiThreadTest.class);
private WebResource webResource1 = resource().path("/test1/path1").queryParam("value", "4");
@Test
@Intermittent(repetition = 30)
public void test_method1_with_intermittent_annotation() {
call(webResource1);
LOGGER.info("method1 executed");
}
@Test
@Intermittent(repetition = 10)
public void test_method2_with_intermittent_annotation() {
call(webResource1);
LOGGER.info("method2 executed");
}
private String call(final WebResource webResource) {
String response = null;
try {
response = webResource.get(String.class);
assertThat("path =" + webResource.getURI() + "response=" + response, !response.isEmpty(), is(true));
LOGGER.info("response=" + response);
} catch (UniformInterfaceException t) {
Assert.fail(t.getMessage());
LOGGER.error("erreur message=" + t.getMessage());
LOGGER.error("erreur réponse=" + t.getResponse());
}
return response;
}
}

#Conclusion

La librairie tempus-fugit nous permet de tester la bonne exécution du code dans un environnement concurrent.
Elle nous offre deux alternatives pour mettre en place une exécution parallèle et répétée des tests unitaires Junit.
{“Tempus-fugit pallie au manque du support natif de Junit des contraintes de concurrence.”} TestNG, l’autre librairie de test, intègre nativement ces problématiques via les attributs threadPoolSize, et invocationCount. A noter que TestNG inclut aussi, contrairement à Junit et tempus-fugit, les attributs timeout et invocationTimeOut pour définir respectivement le temps que le test et l’ensemble des tests doivent prendre au maximum pour être considéré comme positifs.

Tempus-fugit fournit d’autres fonctionnalités, comme entre autres, une gestion facilitée des Threads (interruptions, pause, réveil), une gestion des verrous, et une détection des deadlocks.

Mise à jour du 13/04/2013

Junit permet bien de définir un temps d’exécution maximum par méthode via le paramètre timeout de l’annotation @Test.

1
2
3
4
5
6
//5 secondes maximum
@Test(timeout=500)
public void monTest() {
...
}

Junit permet aussi de définir un temps limite pour tous les tests d’une classe via la @Rule TimeOut.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MaCLasseDeTest {
@Rule
public Timeout globalTimeout = new Timeout(8000); // 8 secondes maximum par méthode de test
@Test
public void monPremierTest() {
......
}
@Test
public void monDeuxiemeTest() {
... }
}
}

Références

Les Classes De Test Ne Sont Pas Des Poubelles !

… ou comment structurer ses tests unitaires.

#Résumé
Je suis souvent confronté à des classes de test d’une longueur gigantesque dans les projets où j’interviens.
Leur contenu en un seul bloc est souvent illisible, quelquefois désorganisé et difficilement maintenable.

J’aborde dans cet article, différentes solutions d’organisation de ses classes de tests en les comparant.

##La solution proposée

Je propose d’organiser les tests via des classes statiques internes couplées à de l’héritage. Vous pourrez regrouper
vos méthodes de test par méthode testée, les exécuter sélectivement et rendre lisible la classe de test.

#Constat

Les classes de test sont, quand les tests sont nombreux, très longues.
Il est difficile de les appréhender d’un seul coup d’oeil.

Pour tester une méthode avec Junit, on crée une nouvelle méthode
dans la classe de test pour chaque nouveau cas d’usage.

Ainsi, les nombreux cas de test impliquent de créer beaucoup de méthodes de test.

##Conséquence

Il est difficile de :

  • retrouver tous les cas de test d’une méthode
  • identifier les cas de test récurrents
  • lancer uniquement tous les cas de test d’une méthode
  • avoir une vue d’ensemble de la classe de test

##Exemple
Voici la classe Foo.java à tester :

{% codeblock lang:java %} public class Foo { public int add(Integer a,Integer b){ return a.intValue()+b.intValue(); } public int substract(Integer a,Integer b){ return a.intValue()-b.intValue(); } } {% endcodeblock %}

Cette classe ne comporte que deux méthodes, prenant chacune deux paramètres.

Malgré sa simplicité, celle-ci comprend pour chaque méthode de nombreux cas à tester :

  • les paramètres sont null
  • un des paramètres est null
  • les paramètres sont négatifs
  • les paramètres sont positifs
  • les paramètres sont pour le premier négatif, pour l’autre positif
  • les paramètres sont pour le premier positif, pour l’autre négatif
  • le résultat est négatif
  • le résultat est positif
  • le résultat est égal à zéro
  • le résultat dépasse la limite inférieure du type Integer
  • le résultat dépasse la limite supérieure du type Integer
  • etc…

En résulte ainsi, un code illisible (je vous épargne le code complet) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class FooTest {
private Foo foo = new Foo();
@Test(expected = NullPointerException.class)
public void testAdd_withNullArguments() throws Exception {
foo.add(null, null);
}
@Test
public void testAdd_withPositiveArguments() throws Exception {
int result = foo.add(3, 4);
assertThat(result,is(7));
}
......
......
......
@Test(expected = NullPointerException.class)
public void testSubstract_withNullArguments() throws Exception {
foo.substract(null, null);
}
@Test
public void testSubstract_withPositiveArguments() throws Exception {
int result = foo.substract(3, 4);
assertThat(result,is(-1));
}
......
......
......
}

Diviser (le code) pour mieux régner

Face à une classe qui grossit à outrance, il est nécessaire de répartir les cas de tests, par exemple suivant la méthode testée.

Essayons de lister les possibilités pour regrouper les cas de test d’une même méthode.

Des débuts d’organisation : les commentaires

Pour mieux organiser ses tests, beaucoup insèrent des commentaires comme repères.

1
2
3
4
5
6
7
8
9
10
////////////// début des tests de la méthode 'add'
.......
.......
///////////// fin des tests de la méthode 'add'
////////////// début des tests de la méthode 'substract'
.......
.......
///////////// fin des tests de la méthode 'substract'

Avantages

Les méthodes comprises entre les blocs de commentaires sont regroupées et associées à la méthode testée.

Inconvénients

Cette solution :

  • rajoute beaucoup de bruit dans la lisibilité de la classe
  • ne resiste pas au reformatage du code effectué avec certains éditeurs de texte
  • ne permet pas de ne choisir d’exécuter qu’un sous-ensemble des tests unitaires

Dans la même optique, Robert Martin (Uncle Bob),
dans son livre Clean Code,
insiste sur l’importance de la lisibilité du code, et recommande d’éviter d’encombrer celui-ci avec ce genre de commentaires. Les commentaires ne permettent pas une bonne orgnaisation du code.

Pas de régions à la .NET en Java

{% pullquote %} A noter que du côté .NET, les directives de régions ont été ajoutées à destination des éditeurs, pour qu'ils puissent cacher ou afficher des parties de code. Les régions sont présentes dans le code source, mais pas dans le MSIL (équivalent du *bytecode* Java). Cette fonctionnalité a donc uniquement pour but d'améliorer la lisibilité, mais ne permet pas de raffiner l'exécution des tests. {"Les régions .NET ne résolvent pas les problèmes d'organisation, et ne sont pas disponible du côté java."} {% endpullquote %}

Une fausse bonne idée : les catégories Junit

Depuis junit 4.8, il est possible d’ajouter des annotations,
afin de regrouper des méthodes (ou des classes) de test en catégories (des sortes de tags).
Plusieurs catégories peuvent être ajoutées à une même méthode ou classe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FooTestWithCategories {
private Foo foo = new Foo();
@Category(Add.class)
@Test(expected = NullPointerException.class)
public void testAdd_withNullArguments() throws Exception {
foo.add(null, null);
}
@Category(Add.class)
@Test
public void testAdd_withPositiveArguments() throws Exception {
int result = foo.add(3, 4);
assertThat(result,is(7));
}
@Category(Substract.class)
@Test(expected = NullPointerException.class)
public void testSubstract_withNullArguments() throws Exception {
foo.substract(null, null);
}
@Category(Substract.class)
@Test
public void testSubstract_withPositiveArguments() throws Exception {
int result = foo.substract(3, 4);
assertThat(result,is(-1));
}
}

Avantages

Les catégories Junit :

  • rattachent les tests aux méthodes testées.
  • permettent de lancer l’exécution de tous les tests d’une méthode particulière, via un Runner Junit nommé Categories, et une sélection des tests via @Categories.IncludeCategory.
1
2
3
4
5
6
7
@RunWith(Categories.class)
@Categories.IncludeCategory(Add.class)
@Suite.SuiteClasses( {FooTestWithCategories.class })
public class MyTestSuite {
}

Inconvénients

Les catégories Junit :

  • ne permettent pas d’isoler les tests d’une méthode par rapport aux autres :
    les tests peuvent avoir les bonnes catégories, mais être éparpillés au sein d’une grande classe de test…
  • elles nécessitent un Runner Junit particulier Categories.Elles ne permettent pas d’utiliser d’autres
    Runner Junit (un seul Runner est spécifié par exécution).
  • l’isolation des cas de test récurrents n’est pas encouragé par cette option.

Les catégories Junit sont plutôt à utiliser pour différencier l’exécution de tests en fonction soit
de leurs dépendances techniques externes (tests d’intégration),de leur rapidité, ou d’un autre critère non fonctionnel.

La solution proposée : utilisation des classes internes

Junit 4.5 apporte l’utilisation d’un Runner permettant de lancer des tests présents
dans des classes statiques internes:
Enclosed, qui est présent dans un package expérimental (org.junit.experimental.runners.Enclosed).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RunWith(Enclosed.class)
public class FooTestWithEnclosed {
private Foo foo = new Foo();
public static class Add {
private Foo foo = new Foo();
@Test(expected = NullPointerException.class)
public void testWithNullArguments() throws Exception {
foo.add(null, null);
}
@Test
public void testwithPositiveArguments() throws Exception {
int result = foo.add(3, 4);
assertThat(result,is(7));
}
}
public static class Substract {
private Foo foo = new Foo();pens
@Test(expected = NullPointerException.class)
public void testWithNullArguments() throws Exception {
foo.substract(null, null);
}
@Test
public void testwithPositiveArguments() throws Exception {
int result = foo.substract(3, 4);
assertThat(result,is(-1));
}
}
}

Avantages

  • rassemble dans une structure toutes les méthodes de tests liées à une méthode testée
  • permet d’exécuter sélectivement les cas de test d’une méthode

Inconvénients

  • le runner Junit est présent dans un package au nom qui implique une existence peu pérenne (experimental)
  • le runner ne lance que des méthodes présentes dans des classes statiques internes. Cet inconvénient ne semble pas gênant, car il semble rarement souhaitable de mélanger des organisations différentes (classes statiques internes et présence directe des méthodes) au sein d’une même classe.

Améliorer la solution : héritage couplé aux classes statiques internes

On vient de voir l’intérêt d’utiliser les classes statiques internes pour regrouper les cas de test d’une méthode.
Afin de clarifier les cas de test récurrents à implémenter pour toute nouvelle méthode, nous pouvons utiliser
des interfaces internes dont héritera toute nouvelle classe statique interne.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RunWith(Enclosed.class)
public class FooTestWithEnclosedAndInheritance {
private Foo foo = new Foo();
private interface MyTest {
void testWithNullArguments();
void testWithPositiveArguments();
}
public static class Add implements MyTest {
private Foo foo = new Foo();
@Test(expected = NullPointerException.class)
public void testWithNullArguments() {
foo.add(null, null);
}
@Test
public void testWithPositiveArguments(){
int result = foo.add(3, 4);
assertThat(result,is(7));
}
}
public static class Substract implements MyTest {
private Foo foo = new Foo();
@Test(expected = NullPointerException.class)
public void testWithNullArguments(){
foo.substract(null, null);
}
@Test
public void testWithPositiveArguments(){
int result = foo.substract(3, 4);
assertThat(result,is(-1));
}
}
}

Avantages

  • Utilise une classe statique interne pour regrouper des cas de test, permet d’utiliser l’héritage pour homogénéiser
    l’implémentation de cas de test récurrents.
  • évite la duplication de code.

Inconvénients

  • si vous utilisez une classe abstraite qui n’a pas de méthodes de test, il est possible que le Runner Junit vous lance une exception.
    Pour régler ce problème, il faut ajouter l’annotation @Ignore à la classe.

Conclusion

Arrêtons d’ignorer les bonnes pratiques de développement dans les tests, avec pour pretexte que ce code n’ira
pas en production ! Le code de test n’est pas du code jetable, mais est intimement lié au code qui sera déployé.

Les qualités du code de test et de production doivent être du même niveau.

Utilisons les structures du langage disponibles comme les classes statiques internes pour répartir les cas de test, et l’héritage pour identifier
les cas de test récurrents.
La non-répétition du code, sa lisibilité, et sa structure cohérente, permettent d’écrire des tests plus robustes,
plus maintenables et compréhensibles !

Références