Java et Predicates : une erreur de conception?

Dans un précédent article sur la programmation fonctionnelle, j'avais présenté les Predicate, utilisables en Java pour filtrer des listes selon des tests prédéfinis.

Il se trouve en réalité que ces objets n'aient pas réellement le comportement attendu... En effet, au lieu de simplement constituer une nouvelle liste à partir de celle fournit en paramètre et en écartant les objets ne satisfaisant pas le prédicat, une erreur est levée à chaque négation :

java.lang.IllegalArgumentException: Cannot add Object - Predicate
rejected it

Je trouve ce principe particulièrement étrange : cela implique que le prédicat ne permettra donc jamais de constituer une nouvelle liste... Pourquoi ne pas se contenter de renvoyer true/false sur le test? La question a également été soulevée sur ce forum, sans vraiment de suite...

Afin de contourner ce problème et d'obtenir réellement le comportement de filtre sur la liste, il faut utiliser l'objet org.apache.commons.collections.iterators.FilterIterator comme ceci :

List<Integer> list = Arrays.asList(5,8,12,11,65,2);
FilterIterator it = new FilterIterator(list.iterator(),
 new Predicate() {
  @Override
  public boolean evaluate(Object arg0) {
   return ((Integer)arg0).intValue()>10;
  }
 });
while(it.hasNext()) {
 System.out.println(it.next);
}
// Affichera : 12,11,65

Donc peut-être ai-je mal compris le fonctionnement des Predicate mais je pense que leur documentation gagnerait à expliciter ce genre de comportement...


Fichier(s) joint(s) :



Xtext et JUnit

Le projet XText de la fondation Eclipse permet de facilement mettre en place des éditeurs de texte pour les langages dédiés (DSLs). Cependant, il est rapidement assez complexe de manipuler l'API de Xtext afin de l'intégrer à une batterie de tests unitaires.

Je vais donc ici fournir un extrait de code pour faciliter la prise en main et l'utilisation, depuis une application externe (JUnit), des moteurs de validation. L'intérêt est donc de pouvoir exécuter les contrôles de Xtext sur nos données et récupérer les éventuels erreurs et/ou warning remontés :

// On parse le texte extrait de l'éditeur
Injector injector = new MyStandaloneSetup().createInjectorAndDoEMFRegistration();
MyParser parser = injector.getInstance(MyParser.class);
InputStream  input = new ByteArrayInputStream(texte.getBytes());
IParseResult result = parser.doParse(input);

// On execute la validation Xtext
Diagnostic diagnostic = Diagnostician.INSTANCE.validate(parseResult.getRootNode().getElement());

// On récupère les erreurs/warning levés
boolean success = true;
String errorMessage = "Message(s) : \n";
switch (diagnostic.getSeverity()) {
case Diagnostic.ERROR:
 success = false;
 if (diagnostic.getException() != null) {
  diagnostic.getException().printStackTrace();
  errorMessage += diagnostic.getException().getMessage();
 } else if (diagnostic.getChildren().size() > 0) {
  for (Diagnostic d : diagnostic.getChildren()) {
   if (d.getException() != null) {
    d.getException().printStackTrace();
    errorMessage += d.getException().getMessage() + "\n";
   } else {
    errorMessage += d.getMessage() + "\n";
   }
  }
 } else {
  errorMessage += diagnostic.getMessage();
 }
 break;
case Diagnostic.WARNING:
 // No warning thrown
}

A partir de ces résultats, il est donc possible de contrôler simplement, avec les assertions JUnit, les retours des validations. Ce code s'inspire du paragraphe Validating Manually de la document XText.

De la même façon, il est possible de lancer l'exécution des switch qui parsent le contenu de l'éditeur afin de le traiter spécifiquement :

// Voir le parseResult instancié plus haut
MySwitch mySwitch = new MySwitch();
mySwitch.doSwitch(parseResult.getRootNode().getElement());

Enjoy!


Fichier(s) joint(s) :



JUnit et tests paramétrés - suite

Dans mon précédent article, j'évoquais le nouveau concept de tests paramétrés proposé par la dernière version du framework JUnit.

La principale "limitation" imposée par ce système réside dans l'architecture même de JUnit à ce niveau : le choix a été fait de rendre statique la méthode fournissant le jeu de données, ce qui est compréhensible puisque le moteur doit bien connaitre les données à tester avant de lancer l'exécution, mais cela implique également une certaine architecture des tests, parfois malhabile pour les maintenir et prévoir leur maintenance ou évolution (surtout quand les tests eux-mêmes doivent paramétrer les paramètres!).

Afin de contourner ce souci, JUnitExt propose une extension de JUnit contenant, parmi d'autres points, la possibilité de paramétrer les tests à partir d'une source de données XML, directement injectée au constructeur du test...

JUnitExt n'a plus l'air très maintenu depuis quelques temps et peut donc poser d'autres problèmes, mais sur ces quelques points il rempli très bien son office d'extension.

Je vous laisse le soin de parcourir le site officiel pour plus d'informations...

HTH!


Fichier(s) joint(s) :



JUnit et tests paramétrés

Dans la plupart des cas, l'écriture de tests JUnit se base sur plusieurs classes, contenant chacune des méthodes représentant les différents cas à tester. Ce concept permet l'organisation de tests visant à mettre à l'épreuve une série de fonctionnalités (métier ou techniques) dans différents scénarios. Or il est parfois nécessaire de réaliser le processus inverse : éprouver le même scénario plusieurs fois à partir d'un jeu de données précis...

Pour cela, le framework propose, dans sa dernière version, un nouveau runner permettant de réaliser ce genre d'action : les tests paramétrés. Voici le code à mettre en place :

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;

import junit.framework.TestCase;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class JUnitParameters extends TestCase {
 
 private File ctlFile;

 protected static Collection<Object[]> filesToTest = new ArrayList<Object[]>();
 
 @Parameters
 public static Collection<Object[]> getTestsFiles() throws Exception {
  return parseFolderForFilesToTest("C:\\JUnit");
 }
 
 protected static Collection<Object[]> parseFolderForFilesToTest(String folderPath) throws Exception {
  File folder = new File(folderPath);
  parseFolder(folder);
  return filesToTest;
 }

 private static void parseFolder(File folder) throws Exception {
  for (File f : folder.listFiles()) {
   if (f.isDirectory()) {
    parseFolder(f);
   } else {
    filesToTest.add(new Object[] { f });
   }
  }
 }
 
 /** Parameters inject by JUnit */
 public JUnitParameters(File file) {
  ctlFile = file;
 }
 
 @Test
 public void testGenerate() {
  assertTrue("Le fichier est vide!", ctlFile.exists() && ctlFile.length()>0);
 }

}

La première annotation @RunWith permet d'indiquer au runner qu'il s'agit d'un test paramétré. Il va donc avant toute action (avant même le @BeforeClass) appeler la méthode marquée par @Parameters afin de constituer le jeu de données. Chaque tableau d'objets est par la suite injecté au constructeur du test, rappelé à chaque jeu de données.

La suite du test s'exécute de manière classique, les méthodes marquées @Test sont appelées successivement par introspection.

Comme on peut le voir ci-dessous, ma classe ne contenant qu'une méthode de test a été exécutée autant de fois que nécessaire :


Fichier(s) joint(s) :