lundi 29 octobre 2012

Ignorer les tests d'intégration lors d'un TimeOut avec les Rules Junit


Contexte

Vous devez développer un client java pour une api distante.
Vous avez bien couvert votre code avec des tests unitaires indépendants de l'infrastructure grâce à Mockito mais on a beau être fan des mocks, ça vous retourne ce que vous attendez et vous testez que vous avez bien reçu ce que vous attendiez, donc par précaution vous décidez de coder quelques tests d'intégrations pour assurer le coup, vérifier que l'api fonctionne comme on l'attends et histoire aussi d'être vite au courant si une montée de version est faite sans compatibilité ascendante.

Problème
De temps en temps, vos tests n'arrivent pas à communiquer avec le server qui héberge l'api et votre server d'intégration continue annonce un build fail et vous vous trouvez dans un des cas suivants:
-> La solution que vous devez appeller est sur un autre SI et vous êtes à la merci des redémarrages, coupures de réseaux et autres sabordages.
-> Votre réseaux local est capricieux



Le grand dilemme

Vous n'aimez pas avoir des tests qui "fails mais c'est normal" ni mettre vos tests en @Ignore et c'est tout à votre honneur. Ce qui est pénible car vous aimez aussi que votre build jenkins soit bleu ou vert.

La solution

J'ai découvert cette semaine les Rules Junit, apparus en version 4.7.
Il est possible d’annoter une Rule Junit publique dans votre class de test, et celle ci interceptera les appels des méthodes de test pour redéfinir son exécution, ignorer certaines exception, vérifier que telle exception a bien été appelée ect.

Pour créer une Rule, il suffit de créer une classe et de lui faire étendre TestRule, apparut en version 4.9 pour remplacer MethodRule :


package com.cafetux.blog.rule;

import java.util.concurrent.TimeoutException;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class TimeOutSilencerRule implements TestRule {

 public Statement apply(Statement base, Description desc) {
  try {
   base.evaluate();
  } catch (Throwable e) {
   if (e instanceof TimeoutException) {
    base = new SilencerStatement();
   }
  }
  return base;
 }
}
Cette règle va jouer le test normalement (base.evaluate() ) mais si une exception est levée, on vérifiera son type. Dans le cas d'une exception java.util.concurrent.TimeoutException, on remplacera le Statement actuel par une Statement fantôme, qui ne fera rien:

package com.cafetux.blog.rule;

import org.junit.runners.model.Statement;

public class SilencerStatement extends Statement {

 @Override
 public void evaluate() throws Throwable {

 }

}
Ainsi le test suivant sera vert:
package com.cafetux.blog.rule;

import java.util.concurrent.TimeoutException;

import org.junit.Rule;
import org.junit.Test;

public class TimeOutSilencerTest {

 @Rule
 public TimeOutSilencerRule timeOutRule = new TimeOutSilencerRule();

 @Test(expected = IllegalArgumentException.class)
 public void test_that_throw_exception_not_silenced() throws Exception {
  throw new IllegalArgumentException("je ne veux pas être ignorée !");
 }

 @Test
 public void test_that_throw_timeout_exception() throws Exception {
  throw new TimeoutException("tango charly le server ne répond plus");
 }

}


L'annotation sur le milshake
Il est possible de choisir quels tests vont êtres traités ou non en créant une annotation. Les annotations peuvent servir aussi à passer des paramètres (une valeur numérique,un message particulier de l'exception,ect):
package com.cafetux.blog.rule;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface IgnoreTimeout {

}
La classe de test sera donc comme ceci:
package com.cafetux.blog.rule;

import java.util.concurrent.TimeoutException;

import org.junit.Rule;
import org.junit.Test;

public class TimeOutSilencerTest {

 @Rule
 public TimeOutSilencerRule timeOutRule = new TimeOutSilencerRule();

 @Test(expected = IllegalArgumentException.class)
 @IgnoreTimeout
 public void test_that_throw_exception_not_silenced() throws Exception {
  throw new IllegalArgumentException("je ne veux pas être ignorée !");
 }

 @Test
 @IgnoreTimeout
 public void test_that_throw_timeout_exception() throws Exception {
  throw new TimeoutException("tango charly le server ne répond plus");
 }

 @Test(expected = TimeoutException.class)
 public void test_that_throw_timeout_exception_but_not_ignored()
   throws Exception {
  throw new TimeoutException("tango charly le server ne répond plus");
 }

}
et l'on notera la nécessité de l'annotation @expected sur le 2e test en timeout pour faire passer les tests, en raison de l’absence de l'annotation @IgnoreTimeout. Pour éviter de rejouer les tests en retournant le même Statement, on cré un Statment qui prend en paramètre une exception et qui la throw lors du evaluate.
package com.cafetux.blog.rule;

import java.util.concurrent.TimeoutException;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import com.cafetux.blog.rule.annotation.IgnoreTimeout;
import com.cafetux.blog.rule.statement.RethrowExceptionStatment;
import com.cafetux.blog.rule.statement.SilencerStatement;

public class TimeOutSilencerRule implements TestRule {

 public Statement apply(Statement base, Description desc) {
  try {

   if (desc.getAnnotation(IgnoreTimeout.class) != null) {
    base.evaluate();
    base = new SilencerStatement();
   }
  } catch (Throwable e) {
   if (e instanceof TimeoutException) {
    base = new SilencerStatement();
   } else {
    base = new RethrowExceptionStatment(e);
   }
  }
  return base;
 }

}



Je n'ai pas trouvé de meilleur solution pour ignorer les tests lors d'un timeout, et suis preneur de toute idée.
Vous trouverez ici les sources du projet:



Aucun commentaire:

Enregistrer un commentaire

Crédits

Thème dérivé du GUI Set Retro-pixel.