Test Logiciel et Assurance Qualité

Aller au contenu | Aller au menu | Aller à la recherche

dimanche 23 mai 2010

5 trucs et astuces pour JUnit

JUnit JUnit est le framework de test unitaires en Java. Il est désormais bien connu des développeurs Java et on le trouve intégré à la majorité des IDE, Eclipse et NetBeans en tête.

Se servir de JUnit est une chose. Ecrire des tests efficaces en est une autre. Ce post n'a pas la prétention de livrer toutes les bonnes pratiques qui entourent JUnit, mais donne quelques trucs qui peuvent servir à un moment ou à un autre.

1- Définissez une classe de base pour vos tests

Un test JUnit doit hériter de junit.framework.TestCase. Cependant, pour un projet donné, on a souvent intérêt à créer une classe de base pour tous les tests JUnit. Par exemple, pour le projet "Titato" :

/**
 * All tests inherit this class.
 */
public abstract clas TitatoTestCase extends 
    junit.framework.TestCase {
}

Puis chaque test du projet hérite de cette classe :

/**
 * Test of ASpecificClass class.
 */
public class ASpecificClassTest extends TitatoTestCase {
  (...)
}

L'intérêt ? Avoir un endroit où définir des méthodes utilitaires qui pourront être utilisées par tous les tests du projet. Par exemple, JUnit ne propose pas d'assertion pour comparer le contenu de tableaux de byte. Si on a souvent besoin d'une telle assertion, TitatoTestCase est l'endroit approprié pour définir une telle méthode : elle sera accessible depuis tous les tests.

2- Pour les larges comparaisons, préférez assertEquals(String, String)

Toutes les assertions se valent... à priori. Pourtant, il y en a une qu'Eclipse traite différemment : la comparaison de chaine. Si on compare deux chaines différentes, Eclipse effectue un diff. Très pratique lorsque les chaines sont longues.

La comparaison :

public void testStrings() {
  assertEquals(
    "This is a string\nAnd this is another one",
    "This is a string\nAnd this is another ONE!");
}

produit :

Comparaison de chaines

Pour tirer parti de cette fonctionnalité, il suffit de rappeler assertEquals(String, String), plutôt qu'une de ses variantes. Prenons une comparaison de liste :

public testGetPeopleList() {
  ArrayList<String> expected = new ArrayList<String>();
  expected.add("John");
  expected.add("Bob");
  expected.add("Paul");
  expected.add("Michael");

  assertEquals(expected, getPeopleList());
}

En exécutant le test tel quel, Eclipse livre un message peu lisible :

junit.framework.AssertionFailedError: expected:<John, Bob, Paul, Michael> but was:<John, Bob, Paul, Greg, Michael>

Il y a une différence, mais quelle est-elle au juste ?

En ramenant les listes à des chaines, on se simplifie la vie. Définissons assertEquals(ArrayList<String>, ArrayList<String>), probablement dans la classe mère des tests JUnit du projet, afin que celle-ci soit utilisable partout :

public void assertEquals(
    ArrayList<String> expected, 
    ArrayList<String> observed) {
  // Let's call assertEquals(String, String)
  assertEquals(toString(expected), toString(observed));
}

private String toString(ArrayList<String> list) {
  String result = "";
  for (String s: list) {
    result += s + "\n";
  }
  return result;
}

Cette fois-ci, l'erreur est évidente :

Assertion pour les ArrayList

Dans toString(ArrayList<String> list), il est important de noter qu'on insère des sauts de ligne entre chaque item de la liste. C'est important puisque cela va avoir un impact sur la présentation du diff. Sans cela, Eclipse affiche un diff portant sur une unique ligne. Nettement moins lisible.

3- Ne prenez pas de risque pour le test d'exception

Les tests négatifs nécessitent régulièrement de vérifier qu'on obtient une exception en cas d'appel erroné :

public void testTouchyMethod {
  try {
    // touchyMethod shall throw a SomeException
    touchyMethod();
    fail("An exception was expected");
  }
  catch(SomeException e) {
    // Correct case
  }
}

Le point faible de ce type de construction vient de l'appel à la méthode fail de JUnit, invoquée si touchyMethod retourne au lieu de lancer une exception. Cet appel est vite oublié. Si tel est le cas, le test ne sert à rien, tout en donnant l'impression inverse.

Afin d'éviter ce risque, on peut implémenter un helper :

public abstract class ExceptionTester extends TestCase {
  public abstract void runTestedAction() throws Throwable;

  public ExceptionTester(Class expectedClass) {
    try {
      runTestedAction();
    }
    catch (Throwable t) {
      assertEquals(expectedClass, t.getClass());
      return;
    }
    fail("Expected " + expectedClass + " but got no error");
  }
}

Puis, lorsqu'on a besoin de vérifier un lancement d'exception :

public void testFailingMethod() {
  new ExceptionTester(Exception.class) {
    @Override
    public void runTestedAction() throws Throwable {
      touchyMethod();
    }
  };
}

Cette seconde version n'est guère plus compacte, mais là n'est pas l'intérêt. Les avantages :

  • Par le jeu de la complétion des IDE modernes, elle est beaucoup plus rapide à saisir. Notamment, Eclipse va déclarer runTestedAction de lui-même.
  • Cette forme est plus sûre : pas d'appel à fail à oublier.

4- Utilisez des fichiers sans dépendre de leur chemin absolu

Un test doit pouvoir d'exécuter dans tout environnement. Lorsqu'on a besoin d'utiliser un fichier à partir d'un test, il est tentant d'utiliser un chemin absolu mais nous savons tous où cela mène...

La solution généralement recommandée est d'utiliser getResource et getResourceAsStream :

InputStream is = this.getClass().getResourceAsStream("data.txt");

C'est un début. Utiliser ces méthodes directement présente néanmoins une difficulté. Comme elles prennent comme référence le répertoire de la classe, le test JUnit dans notre cas, on doit souvent se fendre d'un conséquent "../../../.." avant d'atteindre le répertoire ciblé.

Une bonne technique est de régler ces problème une fois pour toute, dans la classe mère des tests JUnit du projet :

public InputStream getResourceAsStream(String path) 
    throws IOException {
  // Get the path of the JUnit test base class
  URL url = SampleTestCase.class.getResource(
    "TitatoTestCase.class");
  // Get the directory of the class
  // (ie. the TitatoTestCase.class file)
  // This method assume that TitatoTestCase is not located 
  // in a JAR file
  File file = new File(url.getPath());
  // Here, the "/../" depends on the project layout
  return new FileInputStream(file.getParent() + "/../" + path);
}

Cette méthode doit être adaptée pour chaque projet :

  • "TitatoTestCase.class" dépend du nom de la classe de base des tests JUnit.
  • Dans la dernière ligne, "/../" est destiné à se placer au niveau du projet. Ce composant dépend de l'endroit où est compilée la classe ainsi que son package.

Enfin, chaque test peut ouvrir un fichier :

getResourceAsStream("test/test_data/data.txt");

5- Test de séquences complexes : utilisez des traces sous la forme de String

Certain tests consistent à vérifier que telle ou telle méthode a bien été appelée. C'est régulièrement le cas lorsqu'on implémente le design pattern Template method.

Prenons l'exemple d'une classe FileHelper. Cette classe possède une méthode process qui prend en paramètre un nom de fichier ainsi qu'un FileProcessor. FileProcessor est une interface dotée de 3 méthodes :

  • fileOpened(String fileName) : appelée lorsque le fichier à traiter est ouvert ;
  • lineRead(String line) : appelée pour chaque ligne du fichier traité ;
  • fileClosed() : appelé lorsque le fichier traité est fermé.

Lorsqu'on appelle FileHelper.process("myFile.txt", aProcessor), on s'attend à la équence suivante :

  • aProcessor.fileOpened est appelé avec "myFile.txt" ;
  • aProcessor.lineRead est appelé pour chaque ligne du fichier ;
  • aProcessor.fileClosed est appelé.

Comment tester FileHelper.process ? Une solution pratique est de tracer l'exécution au sein d'une instance de FileProcessor et de comparer cette trace avec le résultat attendu.

On commence par créer un fichier d'exemple, nommé data_for_filehelper_process.txt :

This is a sample file
to test FileHelper.process
Bye!

On implémente FileProcessor. Cette implémentation ne fait rien d'autre que sauver la trace des méthodes qui sont appelées :

public class TestProcessor implements FileProcessor {
  String trace = "";
  public void fileOpened(String fileName) {
    trace += "fileOpened(" + fileName + ")\n";
  }
  public void lineRead(String line) {
    trace += "lineRead(" + line + ")\n";
  }
  public void fileClosed() {
    trace += "fileClosed()\n";
  }
  public String getTrace() {
    return trace;
  }
}

Il ne reste plus qu'à implémenter le test :

public void testProcess() {
  TestProcessor processor = new TestProcessor();
  // For an idea of how getResourcePath should work, 
  // see previous section above
  FileHelper.process(getResourcePath(
    "test/test_data/data_for_filehelper_process.txt"), 
    processor);
  // Check the trace
  assertEquals(
    "fileOpened(data_for_filehelper_process.txt)\n" +
    "lineRead(This is a sample file)\n" +
    "lineRead(to test FileHelper.process)\n" +
    "lineRead(Bye!)\n" +
    "fileClosed()\n", 
    processor.getTrace());
}

Si FileHelper.process est buggué et omet de lire la dernière ligne du fichier, on obtiendra sous Eclipse :

Trace testing

Bon test avec JUnit !

dimanche 25 avril 2010

La Journée Française des Tests Logiciels du 30 mars 2010

Journée Française des Tests Logiciels J'ai participé il y a quelques jours à la seconde Journée Français des Tests Logiciels, à deux pas de la Tours Eiffel. Présentations, exposants et petits fours... Bref, une conférence dédiée, fait rare, au test logiciel. Le tout organisé par le CFTL et animé par Bernard Homès, son président.

Les présentations ont été à la hauteur de mes espérances. De bonne qualité et informatives. J'ai hélas raté "Mais qu’est ce qui cloche avec mes métriques ?", proposée par ps_testware et qui a reçu le prix de la meilleure présentation de la journée. Mais les sessions sur les examens du CFTL ou l'intérêt économique du test étaient également très intéressantes. Les participants ont aussi pu discuter avec une quinzaine d'exposants : SSII, spécialistes du test, formateurs... Cela nous a donné l'occasion de partager expériences, points de vue et cartes de visite. J'ai si peu l'occasion de discuter avec des testeurs d'autres sociétés, voilà qui fait plaisir !

Petite déception toutefois : pas de spécialistes de l'embarqué parmi les exposants. Kereval a bien fait une présentation, à laquelle je n'ai pas assisté. Dommage, la lecture des slides à postériori m'a bien plu, peut-être aurais-je du changer mon choix. Smartesting a également des projets dans le domaines de la carte, mais c'est de SAP dont il était question lors de leur présentation. L'embarqué et son représentant le plus populaire, le smartphone, prennent de plus en plus d'importance. Cette relative absence sera j'en suis sûr corrigée lors de la prochaine édition !

Cette journée a été un succès : plus de 300 participants, contre 250 prévus initialement. Pas mal le test ! Au delà de ce signe positif, on a pu sentir une montée de la discipline. Bernard Homès citait un fait intéressant : l'année passée, les embauches de développeurs ont diminué, crise oblige, tandis que celles des testeurs ont un peu augmenté. Simple hasard ? Je ne crois pas. Le test reste trop peu professionnalisé, comme je le regrettais dans un précédent billet. Or les attentes vis à vis des logiciels sont de plus en plus fortes. Plus un poste ou un foyer sans PC. Impossible de ne pas être connecté. Inacceptable de perdre ses données. L'exigence de qualité est la contrepartie du succès du logiciel. Cet aspect est encore en retrait. A nous, testeurs, de relever le défi !

Au vote informel qui s'est tenu en fin de journée pour savoir s'il fallait renouveler la JFTL dès l'année prochaine, c'est une large majorité de "oui" qui l'a emporté. Merci aux organisateurs, et à l'année prochaine !

samedi 13 mars 2010

Tutoriel : Tester une application Android avec Monkey

Android Logo Monkey est un outil qui permet de tester une application Android. Plus précisément, il simule des interactions "aveugles" avec l'application à vérifier. Contrairement à la grande majorité des environnements de tests, Monkey n'a pas besoin d'être piloté, ou alors très sommairement : on ne lui indique pas où cliquer, le résultat attendu, etc. Il génère des interactions quelconques sans ce soucier de la logique applicative et signale les erreurs évidentes, comme les exceptions non traitées.

Ce billet explique comment lancer Monkey et exploiter ses résultats. Il fait référence à une application d'exemple fournie avec le SDK Android.

Préparation de l'environnement

Avant de lancer Monkey, nous avons besoin du SDK Android. Suivez les étapes 1 à 3 d'Installing the SDK. Si vous développez sur Android ou si plus simplement vous avez essayé le Hello World, alors vous avez déjà tout ce qu'il faut.

Nous avons également besoin d'une application à tester. Plutôt que d'en écrire une pour l'occasion, nous allons partir de NotePad, une application d'exemple fournie avec le SDK.

  1. Lancez Eclipse.
  2. Faites File > New > Android Project. Création d'un projet
  3. Dans le wizard, au lieu de créer une nouvelle application, on importe une application d'exemple. Sélectionnez Create project from existing sample, dans le volet Build Target sélectionnez Android 1.6 puis dans la liste Samples choisissez NotePad. Cliquez sur Finish. Wizard Le projet est créé, il apparait dans le Package Explorer, par défaut dans la partie gauche d'Eclipse. Nous allons lancer NotePad, mais pour cela nous avons besoin d'une instance Android virtuelle.
  4. Faites Window > Android SDK and AVD Manager. Android SDK and AVD Manager
  5. Dans la fenêtre Android SDK and AVD Manager, cliquez sur New... Android SDK and AVD Manager - New
  6. Dans la fenêtre Create New AVD, inscrivez Default_AVD en tant que nom, choisissez Android 1.6 - API Level 4 dans la liste Target et entrez une taille de 1024 Mo pour la carte SD. Cliquez sur Create AVD. AVD Creation L'instance virtuelle est maintenant disponible. AVD created Il est tant de lancer Notepad.
  7. Cliquez-droit sur le projet NotesList et sélectionnez Run As > Android Application. Run NotePad L'instance virtuelle se lance et après quelques dizaines de secondes, NotePad est disponible. NotePad
  8. Cliquez sur Menu puis créez une note. NotePad - Add Note
  9. Saisissez du texte et validez. NotePad - Edit Note La note est créée. NotePad - Note Created

Lancer Monkey

NotePad fonctionne et nous pouvons à présent lancer Monkey.

  1. Assurez-vous que l'émulateur est lancé. Si ce n'est pas le cas, suivez les instructions ci-dessus de nouveau pour relancer NotePad.
  2. Ouvrez une console et rendez-vous dans le sous-répertoire tools du SDK Android. New console
  3. Lancez Monkey avec la commande adb shell monkey -p com.example.android.notepad -v 500

Monkey se connecte à l'instance virtuelle et effectue diverses actions, comme le ferait un utilisateur... un utilisateur qui ne sait pas vraiment ce qu'il veut car ses manipulations n'ont aucun but précis : modification hasardeuse de la note existante mais sans sauver, modification du volume, rotation de l'écran... Dans la console, on voit les différentes actions qui sont déclenchées par Monkey tandis que la fenêtre de l'émulateur s'anime au fur et à mesure.

Monkey has finished

Et en cas de bug ?

Monkey vient de jouer avec NotePad sans signaler quoi que ce soit d'anormal. Que se passe-t-il lorsqu'une erreur se produit ? Voyons-voir ça.

  1. Editez NoteEditor.java. Après la ligne 175, insérez throw new RuntimeException("Oh! Un bug!"); afin qu'une exception soit déclenchée lors d'une édition. Insert a bug
  2. Sauvez.
  3. Relancez NotePad. Dans la barre d'outils cliquez sur Run As..., sélectionnez Android Application et faites Ok. Rerun NotePad
  4. A partir de la console, relancez Monkey.

Cette fois-ci, les tentatives de Monkey ne devraient pas se faire sans encombre. Après plus ou moins d'actions, Monkey tombe sur le bug et échoue. Il affiche la stack trace qui permet de retrouver l'origine de l'erreur.

Monkey fails

Le mot de la fin

Monkey a le grand avantage de fonctionner immédiatement, sans nécessiter l'écriture de tests. Naturellement, la contrepartie est cinglante : sans aucune connaissance du fonctionnement attendu de l'application, Monkey ne sait détecter que les erreurs plus bas niveau, celles qui provoquent une récupération par le système. Attention donc à ne pas tomber dans le piège qui consiste à se reposer entièrement sur Monkey, au détriment de véritables tests.

dimanche 28 février 2010

Le test en Ruby on Rails : forfait, hors forfait

Ruby on Rails est un framework web fantastique. Il est puissant, on peut faire de l'Ajax très facilement... Et il a une qualité qu'un compulsif du développement piloté par les tests tel que moi ne peut qu'apprécier : le test fait partie intégrante du framework.

Forfait

Fort heureusement, RoR inclut l'essentiel :

  • Tests des modèles
  • Tests des contrôleurs et des vues
  • Test d'intégration

Mieux : en RoR, les tests sont aussi naturels que le code lui-même. Lorsqu'on crée un modèle ou un contrôleur, RoR crée non seulement ledit contrôleur, mais aussi le test qui va avec.

Controller

RoR inclut également un environnement de test, c'est à dire une configuration et une base de données dédiées au test. Cet environnement est distinct de celui dont on se sert pour faire des essais à la main, de sorte que les tests s'exécutent vraiment "dans leur coin" et sans interférence. La bascule d'un environnement se fait très simplement (pour ne pas dire automatiquement lorsqu'on lance les tests) et sans bidouillage : on ne passe pas son temps à trafiquer un obscur fichier de configuration pour pointer sur une base ou l'autre, ou toute autre manipulation douteuse de ce genre. RoR fait tout cela proprement.

L'environnement de test s'accompagne de fixtures, données qui servent à initialiser la base de test. Des utilisateurs, des commandes, des produits... tout ce qu'il faut pour que les tests puissent démarrer en ayant des données sous la main.

Hors forfait

RoR n'est pas parfait, il manque quelques éléments et certains domaines pourraient être complétés.

Tests des helpers

Les helpers hébergent le code spécifique aux vues qu'on pourrait être tenté d'insérer directement dans les template ERB. Bon choix de design, les helpers permettent de garder les vues aussi légères que possible.

Malheureusement, point de test de helper. Pourtant la génération du contrôleur serait le moment idéal pour créer le test du helper mais non, rien de tel.

Helper

C'est dommage, puisque cela oblige à vérifier le résultat des helpers directement dans les vues, dont le test est plus complexe et fragile. Rien d'insurmontable toutefois, il existe un plugin pour tester les helpers. Et RoR est pardonné pour cet écart.

Test des librairies

Les "librairies" sont le code qu'on place dans le répertoire lib de l'application, souvent parce qu'il n'a sa place dans aucun contrôleur ou modèle et qu'on veut le rendre accessible depuis toute l'application.

RoR n'inclut pas les tests des librairies. En réalité, pour créer un tel test, il suffit de le créer. Le test de lib/my_lib.rb devrait logiquement être test/lib/my_lib_test.rb. RoR aurrait pu faire un petit quelque chose pour lancer les tests sous test/lib avec rake test:libs, ainsi que lorsqu'on lance les tests avec rake test. Qu'à cela ne tienne, un simple ajout dans lib/tasks et le tour est joué.

Tests des migrations

Les migrations sont l'une des bonnes idées de RoR. A vrai dire cela a probablement existé avant RoR mais leur intégration au framework est réussie. Une migration est généralement en charge d'une modification "atomique" sur la base. Typiquement, la création d'une table. Ce peut être également l'ajout de colonnes à une table existante, l'ajout d'index dont on a réalisé l'importance après la mise en production... Ce type de modification ne nécessite pas de test en soit : il suffit d'écrire les tests classiques (unitaires, etc.) qui tourneront sur la base mise à jour.

Certaines migrations font en revanche plus que cela. Elles créent une table pour transformer une relation 1-n en relation n-n, elles modifient une colonne pour stocker non plus des euros mais des centimes d'euro, etc. Dans de tels cas, les migrations ne se contentent pas de nouvelles déclarations, elles itèrent sur les données existantes pour les mettre à jour. Le genre de chose qu'on voudrait tester avant de se lancer en production. RoR ne permet pas de tester les migrations. A ma connaissance, il n'existe pas d'extension pour tester spécifiquement les effets d'une migration.

Fixtures véloces

Les fixtures de RoR sont décrites en Yaml. C'est simple et efficace. Jusqu'à ce qu'elles prennent une certaine ampleur. A partir d'un certain point, les fixtures deviennent délicates à maintenir. Parfois, lorsqu'un test important exige un cas particulier, on se retrouve à modifier de nombreuses fixtures ainsi que certains des tests qui reposaient sur elles.

Des factories se proposent de remplacer les fixtures en Yaml. Celles-ci rendent les données de test plus dynamiques et proche du code. Reste à en faire bon usage, mais c'est un premier pas appréciable.

Un peu plus de vues ?

L'intégration d'un framework de test d'application web haut niveau, comme Selenium, serait un plus. Non pas que RoR ait oublié le test des vues, mais assert_select ne fait pas tout et ne remplace pas un rendu dans un véritable navigateur. Certes l'adhérence à un framework de test spécifique pourrait sembler un peu déplacé de la part de RoR. Mais ses concepteurs ont déjà franchi le pas, avec succès, dans d'autres domaines. Par exemple, RoR utilise Prototype et script.aculo.us, deux frameworks JavaScript qu'on est bien content de trouver déjà tout prêt et bien intégrés dans RoR.

Avertissement

J'ai tenté de dresser un tableau fidèle à la réalité, quoique probablement incomplet. Cependant, Ruby on Rails est un domaine qui évolue très vite et possède de nombreuses ramifications. Il est possible que certaines affirmations soient dépassées. N'hésitez pas à compléter ou rectifier !

samedi 6 février 2010

Deux définitions du test

Comme tant de domaines, le test logiciel a souvent été défini. On trouvera une définition différente dans chaque livre, dans chaque glossaire. Avec un peu de chance, on constatera que ces définitions expriment globalement la même idée. Est-ce bien le cas ?

La définition classique : le test pour vérifier la conformité

Le glossaire du CFTL donne une définition large et conventionnelle du test logiciel :

Processus consistant en toutes les activités du cycle de vie, statiques et dynamiques, concernant la planification et l’évaluation de produits logiciels et produits liés pour déterminer s’ils satisfont aux exigences, pour démontrer qu’ils sont aptes au objectifs et détecter des anomalies.

Voilà une définition qui tache de ne rien oublier : les techniques statiques, si facilement évincées, l'application à tout le cycle de vie du logiciel et non aux activités qui précèdent la mise en production... Quant au but, il est clair : le test permet de vérifier que le logiciel livré et maintenu est le bon. Celui qui est conforme aux attentes des utilisateurs, aux besoins, aux exigences, aux standards... Oh, et s'il y a des défauts, ils sont détectés.

Cette description est assurément positive. Le test permet d'améliorer la qualité. Au fait, comment pourrait-il en être autrement ? Contrairement au développement, le test contribue pas directement au logiciel produit. En passant commande, le client achète du code (fut-il compilé) et de la documentation, pas des tests. Par conséquent, le test n'aurait pas sa place dans les projets s'il n'annonçait pas sa capacité à apporter de la valeur.

La définition destructive : le test qui casse le logiciel

Dans The Art of Software Testing, Glenford Myers donne une toute autre version :

Tester est le processus consistant à exécuter un programme dans le but d'y trouver des erreurs. *

L'approche est radicalement différente. Point de qualité ou de conformité, mais un but précis et résolument négatif. Trouver des bugs. Mettre en défaut le travail des développeurs.

Curieuse approche, voir carrément suicidaire. Entretien d'embauche : "Bonjour Monsieur l'Employeur. Vous devez savoir que j'aime les bugs. Je les collectionne.Si vous m'offrez ce poste, je m'évertuerai à démontrer que vos logiciels sont bourrés d'erreurs". D'accord, cette approche sarcastique ne dupe personne. On voit bien que la définition de Myers a du sens. Néanmoins la question se pose : comment deux définitions si différentes peuvent-elle coexister ?

Une question de buts et de taches

Dans le test comme dans la vie, on a des buts :

  • Vérifier la conformité d'un logiciel
  • Apprendre le chinois
  • Partir en voyage

Cet buts permettent de savoir où l'on va. Cependant, ils ne sont pas réalisables tel quel. Ils ne sont pas actionables comme le décrit David Allen dans son best seller Getting Things Done. On en peut pas débuter la journée en se disant "Aujourd'hui, j'apprends le chinois", car l'apprentissage du chinois prend des années. Pour réaliser ces buts, il faut :

  • Implémenter les cas de test spécifiés la veille, déclencher une réunion pour discuter de ces tests qui n'en finissent pas d'échouer, lancer les tests sur le dernier build...
  • Chercher les cours de chinois qui se déroulent dans le quartier, apprendre 10 idéogrammes, regarder un film chinois en VO...
  • Renouveler son passeport, rechercher le meilleur rapport qualité/prix sur Internet, faire la liste des choses à acheter avant le départ...

Autrement dit, on a besoin de décomposer les buts en sous-projets et finalement en taches qui sont concrètement réalisables. C'est ce que fait la définition de The Art of Software Testing par rapport à celle du CFTL. Un testeur ne peut pas poser les mains sur le clavier en pensant "Je vais vérifier que le logiciel satisfait aux exigences". Il devrait avoir cela en tête pour ne pas perdre le Nord, mais cela ne lui dira pas s'il doit lire un document, ouvrir un fichier ou lancer un test. En approfondissant, le testeur peut déterminer ses activités :

  • Lire et comprendre les exigences, faire des retours s'il y détecte des incohérences
  • Participer à une revue de code
  • ...

L'une de ses activités lui est dictée par la définition de Myers : exercer le logiciel dans le but d'y trouver des erreurs. A nouveau, le testeur devra réfléchir aux taches concrètes qui découlent de ce sous-objectif : écrire un test, exécuter ce test, lire The Art of Software Testing...

* Testing is the process of executing a program with the intent of finding errors.

dimanche 24 janvier 2010

Test négatif d'une vérification de signature ou d'un contrôle d'intégrité

Les signatures jouent un rôle important dans la sécurité des systèmes. Dans la carte à puce par exemple, la présentation d'une signature permettra de charger une nouvelle application ou de lire des données sensibles. Les contrôles d'intégrité ne sont pas en reste. Un problème d'intégrité peut être à l'origine d'une erreur : un fichier qui ne pourra plus être lu, un programme qui ne pourra plus être exécuté... Une défaillance dans l'un ou l'autre de ces domaines peut provoquer une faille de sécurité, une erreur impardonnable pour un système sensible.

Signature et contrôle d'intégrité

De quoi parle-t-on ? Pas d'algorithme spécifique. Dans ce post, point de spécificités de RSA, SHA-1 ou autre. Les signatures et les contrôles ne sont vus que par ce qu'ils ont en commun : des données à protéger, un algorithme quelconque et un sceau, qu'il soit authentifiant ou non.

Algorithme de signature ou de contrôle d'intégrité

Quel que soit l'algorithme utilisé, le principe même d'une signature ou d'un contrôle est que le sceau dépend étroitement des données qu'il protège. A deux données proches mais distinctes correspondent deux sceaux complètement différents.

La signature et le contrôle d'intégrité comportent deux phases :

  • Génération : Les données à protéger sont traitées par l'algorithme et un sceau est généré.
  • Vérification : Le système reçoit les données à vérifier ainsi que le sceau correspondant. Le système régénère le sceau à partir des données et le compare au sceau qu'il a reçu. S'ils diffèrent, il y a un problème quelque part... et les données doivent être rejetées.

Comment tester la vérification ?

L'importance du test négatif

Bien souvent, on attend avant tout d'un système qu'il gère les cas nominaux :

  • Le système interprète correctement la quantité saisie dans le champ "Quantité".
  • L'impression d'un document fonctionne convenablement.
  • ...

Viennent ensuite les cas d'erreur :

  • Le système affiche un message d'erreur lorsque la quantité saisie est négative.
  • L'option d'impression affiche un message spécial lorsqu'il n'y a pas d'imprimante.
  • ...

Ce n'est pas que les cas d'erreur ne soient pas importants, mais à choisir, il vaut mieux qu'on puisse choisir une quantité et imprimer.

Dans le cas d'une vérification d'intégrité ou de signature, le cas d'erreur est aussi important que le cas nominal :

  • Cas nominal : Le système vérifie des données valides. Si cette vérification ne fonctionne pas (dans le sens où elle est buggée), le système va déclarer de faux négatifs, c'est à dire qu'il va rejeter des données pourtant valides. Un problème à ce niveau est bloquant : il y a de bonnes chances pour que rien ne fonctionne, puisque la vérification d'intégrité ou d'authenticité est souvent un préalable à tout autre traitement.
  • Cas d'erreur : Le système vérifie des données invalides. Si la vérification est buggée à ce niveau, le système va déclarer des faux positifs, en acceptant des données corrompues comme si elles étaient valides. Dans cette situation, la vérification échoue dans sa mission de détecter les erreurs. Plus que cela, c'est certainement tout le système qui est compromis, spécialement dans le cas d'une signature. Pour moi qui exerce dans le domaine des cartes à puce, c'est l'un des bugs les plus graves qui puisse survenir.

De plus, le cas nominal est généralement testé par effet de bord. Si le système exige une authentification avant toute autre action, alors la majorité des tests enverront des signatures correctes de sorte à atteindre "l'étape suivante". Pas de seconde chance en revanche pour le cas négatif : rien de viendra remplacer un test explicite.

Assurer que la vérification est faite

Le bug de vérification le plus grossier est... l'absence de vérification. Aussi gros que ce bug puisse être, il est assez plausible :

...
dataToProcess = receiveData();
// TODO: restaurer cette ligne quand l'authentification
// marchera !
//if (!authenticate(dataToProcess)) {
//  throw Exception("Authentication failed!");
//}
processData(dataToProcess);
...

Ou carrément :

...
dataToProcess = receiveData();
processData(dataToProcess);
...

(il n'y a rien à voir : l'authentification a été purement et simplement oubliée)

Non seulement le bug est possible, mais il peut tout à fait passer inaperçu. La vérification d'intégrité ou d'authenticité fait partie de ces fonctionnalités qui ne se manifestent normalement pas, sauf en cas de problème. De ce fait on peut tout à fait passer à côté de ce bug critique.

En soit, tester la vérification est simple. Il suffit d'envoyer un mauvais sceau au système :

data = {1, 2, 3};
seal = generateCorrectSeal(data);
// Ce sceau est correct et accepté par le système
assertTrue(verifySeal(data, seal));
// On corrompt le sceau en inversant tous les bits
for (int i = 0; i < seal.length; i++) {
  seal[i] = seal[i] ^ 0xFF;
}
// Ce sceau corrompu devrait être rejeté
assertFalse(verifySeal(data, seal));

Ce test fait l'affaire. Mais attention à ne pas écrire un test faux !

Dans cet exemple, le test est écrit comme un test unitaire, qui cible un point d'entrée du système, verifySeal. En réalité, ce test est typiquement un candidat pour les tests système. Afin d'éviter le bug de la vérification qui n'est pas faite, on doit faire l'essai sur le système entier et non sur une de ses parties, qui peut justement être présente mais non-sollicitée.

Assurer que la vérification est complète

Un second bug peut affecter la vérification : elle peut être incomplète. Par exemple :

public boolean verifySeal(data, seal) {
  expecedSeal = generateSeal(data);
  // Le sceau est un hash SHA-1
  return compare(expectedSeal, seal, 16);
}

verifySeal génère le sceau qui devrait normalement accompagner les données et le compare au sceau reçu. Le bug se situe dans la longueur de la comparaison. Si le sceau est le résultat d'un hash SHA-1, alors sa longueur est de 20 octets et non 16. La conséquence : les 4 derniers octets du sceau ne sont pas contrôlés. Cela affaiblit la vérification. Dans le cas d'une signature, le bug facilite une attaque force brute.

Le test :

data = {1, 2, 3};
seal = generateCorrectSeal(data);
// Ce sceau est correct et accepté par le système
assertTrue(verifySeal(data, seal));
// On corrompt chaque octet du sceau
for (int i = 0; i < 20; i++) {
  // On corrompt chaque bit de l'octet
  for (int bit = 0; bit < 8; bit++) {
    // On inverse le bit testé
    seal[i] = seal[i] ^ (1 << bit);
    // Ce sceau corrompu devrait être rejeté
    assertFalse(verifySeal(data, seal));
    // On restaure le bit testé
    seal[i] = seal[i] ^ (1 << bit);
  }
}
// Afin de d'éviter un bug de test, on vérifie qu'en
// fin de test seal contient à nouveau le sceau
// attendu
assertTrue(verifySeal(data, seal));

Assurer que toutes les données sont vérifiées

La vérification peut être incomplète en ne portant que sur une partie du sceau. On peut imaginer une bug similaire sur les données elles-même. Il suffit d'une erreur de bornes pour qu'une partie des données ne soient pas impliquées dans la génération du sceau.

Un tel bug sera détecté sans qu'un test spécifique soit nécessaire. D'un côté, un test prouve déjà que la vérification est effectuée. De l'autre, il y a forcément de nombreux cas positifs engendrés par d'autres tests qui veulent simplement "passer" la phase de vérification pour atteindre d'autres fonctionnalités du système. La combinaison de ces deux éléments permettent de s'assurer que les sceaux sont correctement calculés par le système. La nature de ces sceaux, qu'ils soient CRC, hash ou signatures, fait qu'on sait que toutes les données sont prises en compte dans la vérification : si, disons, le dernier octet des données était systématiquement ignoré, le sceau généré serait très différent de celui attendu et généré par les tests.

dimanche 10 janvier 2010

Tutoriel Selenium

Logo Selenium SeleniumHQ est un outil de test pour les applications Web. Il permet de vérifier qu'une application se comportera de la même façon quel que soit le navigateur utilisé. Selenium s'utilise ainsi :

  1. Enregistrement d'une séquence : A l'aide d'un plugin Firefox, on enregistre une séquence. Clic sur un lien, saisi d'un formulaire, vérification d'une réponse...
  2. Intégration du test : On intègre le test au sein d'une suite de test, dans le langage et le framework de son choix.
  3. Exécution du test : Le test est joué sur plusieurs navigateurs et plusieurs plateformes afin de valider l'interopérabilité de l'application.

Selenium est indépendant du langage utilisé. On peut tester une application PHP, Java, Ruby on Rails... De plus, Selenium supporte les langages les plus communs pour la programmation Web :

  • C#
  • Java
  • Perl
  • PHP
  • Python
  • Ruby

En plus des langages, Selenium connait les frameworks les plus utilisés. Par exemple, en Ruby on Rails, on pourra générer des tests Test::Unit ou RSpec, au choix. De cette façon, on obtient des tests consistants avec le reste des tests écrits pour l'application.

Dans ce tutoriel, plutôt que de développer une application pour l'occasion, nous allons tester une application existante : le moteur de recherche de Google (un exemple très inspiré, d'autant que c'est aussi l'exemple choisi par les développeurs de Selenium). Nous allons générer un test JUnit que nous allons intégrer au sein d'une suite de test, puis nous exécuterons ce test.

Installation

Nous allons avoir besoin de Selenium IDE :

  1. Lancez Firefox
  2. Allez sur la page de téléchargement de Selenium
  3. Sélectionnez le téléchargement de Selenium IDE Page de téléchargement
  4. Le téléchargement fait apparaitre un bandeau Firefox a empêché ce site (seleniumhq.org) d'installer un logiciel sur votre ordinateur en haut de la page. Cliquez sur Autoriser pour permettre l'installation. Installation bloquée
  5. Dans le popup Installation d'un logiciel, cliquez sur Installer maintenant. Installation d'un logiciel
  6. Dans le popup Modules complémentaires, cliquez sur Redémarrer Firefox pour terminer l'installation. Modules complémentaires

Nous allons aussi utiliser Selenium RC pour exécuter le test :

  1. Allez sur la page de téléchargement de Selenium
  2. Téléchargez Selenium RC Téléchargement de Selenium RC
  3. Décompressez l'archive téléchargée dans le répertoire de votre choix. Par exemple, dans C:\Program Files (x86) (auquel cas Selenium RC est installé dans C:\Program Files (x86)\selenium-remote-control-1.0.1)

Puisque nous allons utiliser JUnit, nous l'installons également :

  1. Allez sur la page de téléchargement de JUnit
  2. Téléchargez le JAR de JUnit Téléchargement de JUnit
  3. Copiez le JAR de JUnit tel quel dans le répertoire de votre choix (il ne faut pas le décompresser). Par exemple, dans C:\Program Files (x86)\junit-4-8-1

Enregistrement

L'installation terminée, nous pouvons créer notre premier test. La première étape consiste à enregistrer la séquence que nous voulons automatiser.

Dans cet exemple, nous testons le moteur de recherche de Google et écrivons un test qui s'assure que le site de Selenium (seleniumhq.org) fait partie des résultats de la première page lorsqu'on lance une recherche Google sur "selenium". L'objet de ce test est évidemment discutable : si le site de Selenium n'apparait pas sur la première page, cela peut être dû à un mauvais référencement de ce dernier, plutôt qu'à un authentique bug de Google. Dans le cadre de ce tutoriel, c'est bien suffisant.

Pour enregistrer la séquence :

  1. Lancez Firefox
  2. Allez sur la page d'accueil de Google
  3. Lancez Selenium IDE à partir de Firefox, dans le menu Outils Lancement de Selenium IDE
  4. Disposez Firefox et Selenium IDE de sorte à voir les deux fenêtres simultanément. Cela n'a rien d'obligatoire, mais il est pratique de voir la fenêtre de Selenium mise à jour au fur et à mesure de l'enregistrement du test.
  5. Saisissez l'URL de la page d'accueil de Google dans le champ Base URL de Selenium IDE. En effet, cette page sera le point de départ de notre test. Disposition Selenium IDE / Firefox
  6. Lancez l'enregistrement en cliquant sur le bouton Record de Selenium IDE. Selenium est maintenant en train d'enregistrer les actions de l'utilisateur dans Firefox : click, saisie... Lancement de l'enregistrement
  7. Jouez la séquence à enregistrer. Saisissez "selenium" dans la page de Google et cliquez sur Recherche Google. On constate que Selenium IDE liste les actions au fur et à mesure. Exécution de la séquence
  8. Ajoutez une assertion. Dans la page de résultat de Google, nous voulons nous assurer qu'un lien vers le site de Selenium est présent. Il apparait effectivement, on peut voir le texte "seleniumhq.org" qui apparait au bas du premier résultat. Sélectionnez ce texte dans Firefox, faites un clic droit et sélectionnez verifyTextPresent seleniumhq.org Ajout d'assertion
  9. Stoppez l'enregistrement. Dans la fenêtre de Selenium IDE, cliquez de nouveau sur le bouton Record pour arrêter l'enregistrement. On constate que Selenium a enregistré la séquence que nous venons de jouer : open / ouvre la page de Google, type q selenium saisit "selenium" dans le champ texte de la page d'accueil de Google, clickAndWait btnG clique sur le bouton Recherche Google et attend la page de résultat et verifyTextPresent seleniumhq.org s'assure que le texte "seleniumhq.org" est bien présent dans la page de résultat. Arrêt de l'enregistrement
  10. Rejouez le test. Maintenant que Selenium a enregistré la séquence, nous pouvons la rejouer à loisir. Cliquez sur le bouton Play entire test suite. Selenium IDE joue le test "pour de vrai" en pilotant Firefox. On voit ce dernier aller sur la page d'accueil de Google, saisir "selenium" et charger la page de résultat. Selenium IDE liste ce qu'il a exécuté dans l'onglet Log. On constate que l'assertion finale est également évaluée : si "seleniumhq.org" n'apparaissait pas dans la page de recherche, Selenium le signalerait et le test échouerait. Exécution du test

Intégration

Pour le moment, nous avons un test que nous pouvons jouer à partir d'une interface graphique. C'est un début, mais il faut aller plus loin :

  • Exécution dans d'autres navigateurs : Développer avec Firefox pourquoi pas, mais il faut s'assurer que l'application fonctionne aussi avec Internet Explorer, Safari et les autres.
  • Gérer de nombreux tests : Dans ce tutoriel, on se contente d'un test. Dans un cas réel, on a plusieurs dizaines, centaines voir milliers de tests. Cette interface graphique n'est pas viable pour un tel usage.
  • Tests intégrés : Jouer le test à partir de l'interface graphique est pratique lorsqu'on le met au point. En revanche, cela n'est plus praticable lorsqu'on veut jouer le test au même titre que les autres tests de l'application (tests unitaires, etc.). Les tests générés avec Selenium doivent pouvoir être lancés dans le cadre d'une intégration continue, etc.
  • Modification et déclinaison du test : Nous pourrions souhaiter décliner notre test de plusieurs façons. Par exemple, en recherchant différents termes. Avec Selenium IDE, notre seule option est de jouer le scénario encore et encore pour créer nos tests. Cela va vite devenir ennuyeux.

L'étape d'intégration est très rapide car Selenium génère directement un test JUnit pour nous :

  1. Dans Selenium IDE, sélectionnez Options > Format > Java (JUnit). Selenium affiche alors notre test au format JUnit. On reconnait les dfférentes étapes, mais "à la Java" : selenium.open("/") pour ouvrir la page d'acueil de Google, etc. Sélection du format
  2. Copiez le contenu du test, collez-le dans un fichier SearchTest.java que vous créez n'importe où.
  3. Supprimez la mention du package. Dans Search.java, supprimez la ligne de déclaration du package (package com.example.tests;). Dans un cas réel, nous devrions spécifier un package relatif à l'entité pour laquelle nous développons. Dans le cadre de cet exemple, nous allons au plus simple.
  4. Changer le nom de la classe pour SearchTest. Autrement dit, public class Untitled devient public class SearchTest.
  5. Changer le nom de la méthode de test pour testSearch.

Le test définitif :

import com.thoughtworks.selenium.*;
import java.util.regex.Pattern;

public class SearchTest extends SeleneseTestCase {
  public void setUp() throws Exception {
    setUp("http://www.google.fr/", "*chrome");
  }
  public void testSearch () throws Exception {
    selenium.open("/");
    selenium.type("q", "selenium");
    selenium.click("btnG");
    selenium.waitForPageToLoad("30000");
    verifyTrue(selenium.isTextPresent("seleniumhq.org"));
  }
}

A présent nous compilons ce test. La séquence qui suit est pour Windows. Si vous utilisez un autre environnement, vous devrez adapter certaines commandes :

  1. Ouvrez une console. Faites Touche Windows + R, puis demandez à lancer le programme cmd.
  2. Définissez l'emplacement de Selenium RC en définissant la variable SELENIUM_RC_HOME. Par exemple, set SELENIUM_RC_HOME=C:\Program Files (x86)\selenium-remote-control-1.0.1
  3. Définissez l'emplacement de JUnit en définissant la variable JUNIT_HOME. Par exemple, set JUNIT_HOME=C:\Program Files (x86)\JUnit-4-8-1
  4. Placez-vous dans le répertoire où vous avez créé SearchTest.java
  5. Compilez le test avec la commande javac -cp "%JUNIT_HOME%\junit-4.8.1.jar;%SELENIUM_RC_HOME%\selenium-java-client-driver-1.0.1\selenium-java-client-driver.jar" SearchTest.java. Vous devrez peut-être adapter cette commande si vous avez téléchargé une version plus récente de JUnit ou Selenium RC.

Le test est désormais intégré aux autres tests unitaires de l'application. De plus, comme il s'agit de véritable code et non d'une séquence figée, nous pouvons modifier le test pour jouer la séquence plusieurs fois, par exemple avec différents termes de recherche.

Exécution

Maintenant que nous avons un test compilé, nous pouvons l'excuter. Dans ce post, nous ne verrons qu'une façon de le lancer. Cependant, il existe plusieurs options, selon ce que nous attendons de nos tests.

Exécution simple

C'est le mode par défaut, celui qu'utilisent les développeurs pour vérifier leurs réalisations. Le test est lancé depuis la ligne de commande ou depuis un IDE et le résultat est utilisé sur le champ : si bug il y a, il est fixé !

Lorqu'on exécute un test avec Selenium, celui-ci est joué dans un véritable navigateur Web, et non par envoi direct de requêtes HTTP, par exemple. Pour réaliser cela, Selenium utilise un serveur. Ce serveur reçoit les requêtes du test ("ouvrir une page", "cliquer", etc.) et ouvre un navigateur qu'il pilote pour exécuter les commandes du test. Par conséquent, pour lancer SearchTest, nous devons d'abord lancer le serveur Selenium :

  1. Ouvrez une nouvelle console.
  2. Définissez l'emplacement de Selenium RC en définissant la variable SELENIUM_RC_HOME. Par exemple, set SELENIUM_RC_HOME=C:\Program Files (x86)\selenium-remote-control-1.0.1
  3. Ajouter Firefox dans votre PATH. Par exemple, si Firefox est installé dans C:\Program Files (x86)\Mozilla Firefox, utilisez set PATH=%PATH%;C:\Program Files (x86)\Mozilla Firefox
  4. Lancez le server Selnium avec la commande java -jar "%SELENIUM_RC_HOME%\selenium-server-1.0.1\selenium-server.jar"

Nous sommes maintenant prêts à lancer le test :

  1. Reprenez la console que vous avez utilisée pour compiler le test, celle où JUNIT_HOME et SELENIUM_RC_HOME sont définies et pour laquelle le répertoire courant contient le test compilé.
  2. Lancez le test avec la commande java -classpath "%JUNIT_HOME%\junit-4.8.1.jar;%SELENIUM_RC_HOME%\selenium-java-client-driver-1.0.1\selenium-java-client-driver.jar;." junit.textui.TestRunner SearchTest

A l'exécution de cette dernière commande, Selenium RC lance une instance de Firefox et on peut voir le test se dérouler. Cela étant dit, ce n'est pas de voir le navigateur qui nous importe, mais le résultat : à la fin de l'exécution, SearchTest est passed, comme tout test JUnit exécuté avec succès. Il n'y a pas d'intervention manuelle.

Validation, Régression, Intégration continue...

Au delà de l'exécution simple, Selenium permet d'exécuter des tests automatisés à toutes les étapes de la vie du projet :

  • Intégration Continue. Ces tests sont de parfaits condidats pour une exécution fréquente et programmée : toutes les nuits, toutes les semaines...
  • Test de Régression. Comme ils sont automatisés, ces tests peuvent être utilisés par les développeurs comme tests de régression, jouables facilement et rapidement.
  • Test de validation. Naturellement, on peut se servir de ces tests pour valider l'application avant livraison. Une fois encore, l'automatisation fait qu'on ne devrait pas se priver de les jouer.
  • ...

Test d'interopérabilité

Une particularité de Selenium est de lancer les tests sur un véritable navigateur. Comme celui-ci est capable de gérer les principaux navigateurs et qu'il peut s'exécuter sur les principales plateformes, il est l'outil idéal pour vérifier l'interopérabilité de l'application : "Si mon test passe avec succès à partir de Firefox exécuté sous Windows, le fera-t-il avec Safari exécuté sous Mac ?" Selenium permet de répondre à cette question, une fois de plus de façon automatisée.

Le test d'interopérabilité peut être "fait maison" : on se procure machines et navigateurs, on lance différents serveurs Selenium et le tour est joué. Cela demande toutefois pas mal d'efforts : gestion de plateformes différentes, gestion des ressources (deux testeurs ne doivent pas utiliser le même serveur en même temps)... Des entreprises fournissent des solutions de test à la demande pour éviter ces problèmes.

Test de charge

Les tests Selenium peuvent servir de base au test de charge. Au lieu de jouer un test à la fois, on en exécute... un grand nombre ! Comme pour le test d'interopérabilité, ce principe nécessite une certaine organisation pour être mis en œuvre.

Selenium est un outil puissant, élégant et facile à prendre en main. L'essayer c'est l'adopter ! Faites l'essai !

jeudi 24 décembre 2009

Données de test : Attention aux pièges !

Lorsqu'on teste, on a souvent besoin de données factices. Dans ce post, ce que j'appelle "données factices" sont des octets quelconques, sans sens particulier. Dans un certain nombre de cas, le contenu n'a pas d'importance, comme dans l'exemple suivant :

// Testons les fonctions write(file, dataToWrite) et 
// read(file, outputBuffer) 
// On devrait pouvoir lire ce qu'on vient d'écrire
// dans FILE_1
testData = { ??? }
write(FILE_1, testData)
read(FILE_1, readData)
// Vérification que testData et readData
// contiennent la même chose
assertEquals(testData, readData)

read et write ne se préoccupent pas des données qu'elle manipulent. Ce cas de test fonctionne quel que soit la valeur de testData. Est-ce que cela signifie qu'on prendre n'importe quoi ? Certainement pas.

Remplissage avec des zéros

testData = {0, 0, 0, ... }

Le remplissage avec des zéros est le comportement par défaut dans un certain nombre de langages. Par exemple, en Java :

testData = new byte[10];
// testData vaut {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}

On peut utiliser n'importe quel contenu pour ce test, alors pourquoi s'embêter ? Pour répondre à cette question, reformulons le problème. read et write peuvent être bugguées. Nous avons un test pour ça. Le seul élément qui nous manque est testData. La question devient :

  • Y a-t-il un bug potentiel qui pourrait échapper à notre test en raison du choix de testData ?
  • Quelle est la probabilité de ce bug ?

On pourrait imaginer plusieurs implémentations. Celle-ci est suffisante :

write(file, dataToWrite) {
  // TODO
}

read(file, outputBuffer) {
  // TODO
}

En fait... read et write ne sont pas implémentées. Elles ne font rien et attendent qu'un développeur bienveillant les code. Si le produit est livré en l'état, on peut s'attendre à un bug à moment ou à un autre !

Évidemment, notre test devrait repérer cette erreur grossière. Que se passe-t-il si on l'exécute ? Rien. Il s'achève en un "PASSED" tout à fait rassurant. testData est rempli avec des zéros et il en est de même pour readData dans un langage comme Java si on ne prend pas la précaution de l'initialiser (et pourquoi le ferait-on ? read est supposé l'écrire après tout). Par conséquent, l'assertion finale compare deux tableaux identiques ({0, 0, ...}) et passe.

Le pattern "zero" est vraiment, vraiment mauvais. Au suivant !

Remplissage avec une unique valeur

testData = {0x95, 0x95, 0x95, ... }

Le zéro était une mauvaise idée. Qu'en est-il d'une autre valeur ? Même approche, voici le bug :

write(file, dataToWrite) {
  // TODO
}

read(file, outputBuffer) {
  fill(outputBuffer, 0x95)
}

A nouveau, le bug est facile à obtenir : write ne fait rien et read remplit le buffer avec 0x95, peu importe ce qu'on lui demande. Cependant, la situation est bien différente de l'exemple précédent. Oublier d'implémenter deux fonctions est une erreur tout à fait plausible. Mais que penser de fill(outputBuffer, 0x95) dans la seconde version ? Cette instruction est étrange. C'est plus qu'un oubli ou une incompréhension du développeur. L'hypothèse du programmeur compétent dit que cette erreur est peu probable.

Bien, une valeur non-nulle est meilleure que zéro, mais est-elle bonne pour autant ? Non, toujours pas :

write(file, dataToWrite) {
  for i = 0... dataToWrite.length - 1 {
    internalBuffer[file][i] = dataToWrite[0]
  }
}

read(file, outputBuffer) {
  for i = 0... dataToWrite.length - 1 {
    outputBuffer[i] = internalBuffer[file][i]
  }
}

Cette fois-ci, read et write font quelque chose de sensé. write copie les données dans un buffer interne et read fait l'opération inverse. Mais une fois encore, il y a un bug, tout à fait plausible cette fois-ci : au lieu de prendre l'octet à l'offset i, write copie toujours le premier octet de dataToWrite. Et comme tous les octets de dataToWrite sont identiques dans notre test, le bug ne sera pas découvert.

Le pattern basé sur une répétition est un mauvais choix pour une situation type "système de fichier" comme celle-ci. En plaçant tous les octets sur un plan d'égalité, il tend à annuler les nombreuses erreurs d'offset et longueur qui peuvent se produire.

Valeur = Index

testData = {0, 1, 2, ... }

Utiliser une seule valeur n'est pas efficace. L'étape suivante est d'utiliser plusieurs valeurs. Un tableau du type {0, 1, 2, ... } pourrait être un bon candidat. Il a l'avantage d'être facile à construire :

for i = 0... testData.length - 1 {
  testData[i] = i
}

Et... voici le bug :

write(file, dataToWrite) {
  for i = 0... dataToWrite.length - 1 {
    internalBuffer[file][i] = dataToWrite[i]
  }
}

read(file, outputBuffer) {
  for i = 0... dataToWrite.length - 1 {
    outputBuffer[i] = i
  }
}

Au lieu de copier le contenu de internalBuffer[file], read prend i, l'offset lui-même. Dans ce simple exemple, le développeur n'a que peu de chances de se tromper de la sorte. Dans un vrai système cela peut facilement se produire et on ne peut pas se permettre de passer à côté.

La racine du problème est la relation entre le contenu de testData et un autre facteur utilisé dans le code (l'offset i dans cet exemple). Une erreur du programme peut ainsi échapper au test parce que le choix de testData suit le même chemin.

Données aléatoires

testData = {random(), random(), random(), ... }

Remplir testData avec des données aléatoires est la première bonne proposition. Les octets ne sont pas constants ou liés à quelque chose comme l'offset, la longueur du fichier ou un identifiant. Cela prévient le risque de confusion. Je n'ai aucun exemple de bug réaliste cette fois-ci.

Les valeurs aléatoires soulèvent des questions et engendre parfois des débats sans fin. Le test ne produira jamais deux fois le même testData d'une exécution à l'autre. Si un bug est découvert, il pourrait ne plus être détecté lors de l'exécution suivante. Pour cette raison, les données aléatoires ont leurs détracteurs. Il existent des solutions de rechange, à discuter dans un autre post.

samedi 19 décembre 2009

Développement piloté par les tests - 5 idées reçues

La méthode du développement piloté par les tests (Test Driven Development, TDD) dit d'écrire les tests unitaires en même temps que le code. On répète le cycle suivant :

  • Écriture d'un test unitaire, alors que le code testé n'existe pas encore.
  • Exécution du test. Il doit échouer.
  • Écriture du code.
  • Exécution du test. Cette fois-ci il doit passer.

J'ai expérimenté le développement piloté par les tests il y a quelques années avec l'outil emblématique de cette pratique, JUnit. Je suis depuis un inconditionnel, que ça soit en Java ou en Ruby on Rails. J'ai naturellement essayé de convertir mon entourage au TDD. L'évangéliste occasionnel que je suis a été confronté à quelques préjugés. Parfois fausses ou plus souvent exagérées, il convient de démonter les idées préconçues.

1. Ce n'est pas mon travail

Une réaction bien naturelle lorsqu'on travaille dans une équipe qui compte des testeurs. Si un développeur écrit des tests, ne fait-il pas le travail d'un autre ? N'est-il pas en train de se disperser ? Pas du tout.

Il est entendu qu'un développeur commet des erreurs. On les retrouvera sous la forme de bugs dans le code. Cependant, il y a aussi une qualité minimale qu'on attend du développeur. D'abord, le code doit compiler. On imagine mal un développeur qui livrerait à ses collègues un code contenant des erreurs de syntaxe. Et puis, bien que ça ne soit pas la règle dans toutes les sociétés, le code doit fonctionner. On ne parle pas d'un fonctionnement qui satisfasse toutes les parties prenantes, ce sont les testeurs qui s'en assureront. Mais de quelque chose "qui tourne".

C'est le premier objectif du TDD. Écrire un code qui marche. Plus précisément, un code conforme aux intentions du développeur. Ne vous inquiétez pas pour les testeurs, ils devront toujours vérifier que le logiciel est conforme aux exigences, qu'il est robuste, performant...

2. Tester prend du temps

Le calcul est vite fait. Avec le TDD, on écrit le code et les tests. Sans, on se contente du code. Alors, le TDD prend plus de temps ? Sans doute, mais beaucoup moins qu'on pourrait le penser.

En réalité, on se contente rarement d'écrire du code puisque, on l'a vu, on veut s'assurer qu'il fonctionne. La méthode la plus primaire, mais tellement naturelle, consiste à exécuter le code à la main. Le développeur lance le programme qu'il écrit et interagit pour en vérifier le comportement. Dans le cas d'une librairie, il ira même jusqu'à écrire un petit programme qui l'utilise. Simple.

Ces essais prennent du temps, surtout lorsque la portion de code à tester est difficile à atteindre. Par exemple, si on essaie l'achat sur un site de e-commerce, il faut à chaque fois ajouter des éléments au panier, entrer un nom et une adresse postale, etc. Espérons qu'il n'y ait pas trop de bugs !

Alors oui, le TDD prend du temps, mais il convient de faire ses comptes en considérant tous les paramètres.

3. Tester ou faire des essais, ça revient au même

Écrire un test ou faire des essais, deux façons de parvenir au même résultat : un programme qui se comporte correctement. Dans un premier temps c'est vrai, mais à plus long terme il y a une grande différence.

Les essais sont vites oubliés. Lorsqu'on écrit un programme pour tester une librairie, on n'a généralement aucune velléité de réutilisation. Pas de commentaires, un code sans structure réfléchie, des "print" ça et là pour que le résultat soit visuel... Dès que le développement est fini, tout cela est jeté.

Les tests unitaires, eux, sont là pour rester. Les méthodes agiles s'en servent de suites de non-régression, indispensables lors des épisodes de refactoring. Quel que soit le cycle de développement, on trouve de nombreuses utilités aux tests unitaires écrits des mois auparavant.

4. Tester est ennuyeux

Le test a parfois mauvaise presse. Rappelons d'abord qu'en TDD, on parle de tests automatisés, et non de tests manuels. Cela est donc moins pénible et intellectuellement plus satisfaisant.

Une fois de plus, les tests sont à comparer aux essais auxquels on devra se livrer en leur absence. Faire des essais peut être très simple, ce qui constitue un avantage. En dehors de ce scénario qui se produit trop rarement, faire des essais lasse rapidement.

Les tests unitaires quant à eux sont de vrais programmes, destinés à être conservés et écrits dans les règles de l'art. En ce sens, ils apportent la même satisfaction que le code bien architecturé, indenté et commenté. Parfait pour contenter les perfectionnistes que nous sommes souvent.

5. Tester ne paie qu'à moyen terme

Un de ces jours, les tests serviront de suite de non-régression. Mais là, maintenant, tout de suite, il y a du code à écrire et les tests ne servent à rien. Autant coder sans interruption et faire un seul essai final.

Il me semble que les pratiques du développement n'évoluent pas dans ce sens. Ces dernières années ont vu la fin des erreurs de compilation... pour certains langages en tout cas. Autrefois, les éditeurs de code ne proposaient qu'une coloration syntaxique rudimentaire et chaque compilation venait avec son lot de surprises : une parenthèse manquante par ci, une faute de frappe par là. On pouvait consacrer une partie de sa journée à corriger ces erreurs. Des IDE comme Eclipse se sont depuis imposés et les éditeurs de quelques langages comme Java sont évolués au point que chaque erreur de compilation est signalée sitôt commise. Les corrections interviennent au fur et à mesure et la compilation est désormais une formalité.

Ce que les IDE ont fait pour la compilation, le TDD le fait pour le fonctionnement. Bien sûr, on commet toujours les même bugs, mais ceux-ci sont détectés et corrigés sur le champ. On a le sentiment d'écrire du code qui marche du premier coup, ce qui est très appréciable. Inutile d'attendre pour bénéficier des tests, ils paient immédiatement.

Et aussi...

L'énumération de ces idées reçues est l'occasion d'évoquer des avantages du développement piloté par les tests : constitution d'une suite de test, satisfaction du travail réalisé... Le TDD a également d'autres atouts, comme celui de concevoir du code testable dès le départ. On se surprend souvent à concevoir du code mieux structuré et moins monolithique justement parce que le test correspondant semblait trop difficile à écrire de prime abord.

Il existe de nombreuses ressources sur le Web à ce sujet et quelques ouvrages. À vous de jouer, à vous de convaincre vos collègues !

samedi 5 décembre 2009

Qu'ai-je appris sur le test logiciel en tant qu'étudiant ?

Cinq années d'études pour devenir Ingénieur. Comment ont-elles été mises à profit ?

Développement

Passed

Côté développement, aucun doute, le compte y est. Des cours de Pascal, C, C++, Java, mais aussi des cours algorithmique, d'intelligence artificielle, de conception d'interface graphique... Je ne vais pas plonger dans mes archives pour faire le compte, mais il doit bien y avoir quelques centaines d'heures de cours et de TD sur le sujet. A cela s'ajoutent les projets. Sans doute autant de temps passé le soir et les weekend, seul ou en groupe, chez soi ou en "salle Info" à coder et rédiger des rapports. Quant aux projets personnels, eux-aussi contribuent à l'apprentissage. Qu'ils soient réalistes ou utopiques, tout juste entamés ou achevés, ces réalisations apportent beaucoup aux passionnés. Enfin, les stages sont l'occasion de pratiquer la programmation et forgent l'expérience.

De manière générale, les ingénieurs en informatique assidus sont des développeurs tout à fait compétents à la fin de leurs études.

Gestion de projet

Passed

La gestion de projet est un autre sujet important. Notre travail s'inscrit toujours dans le cadre d'un projet, quel que soit sa forme. Il est donc important d'en maitriser les rudiments. De plus, la gestion de projet est l'une des évolutions naturelles de l'ingénieur (l'autre étant l'expertise technique). Nos études devraient logiquement nous y préparer.

J'ai reçu quelques cours de gestion de projet, parfois teintés de Merise. On m'a évidemment présenté le cycle en V, et les méthodes itératives. Quant aux méthodes agiles, elles étaient sans doute un peu trop récentes. Quelques travaux pratiques étaient au programme, avec rédaction de cahier des charges et autres. Tout cela a du représenter une centaine d'heures. Par contre, pas de planification, d'estimation de charge ou de diagramme de Gantt.

Et pour ce qui est des à côtés, le constat est là-aussi bien différent. Si beaucoup d'entre nous ont volontiers appris de nouveaux langages et environnements simplement par passion, moins se sont lancés dans la gestion de projet pure juste par goût.

Faut-il conclure que la gestion de projet ne m'a pas été suffisamment enseignée ? Je ne pense pas. On ne commence jamais sa carrière en tant que chef de projet ou assimilé. Études ou pas, il est nécessaire d'acquérir de l'expérience sur le terrain. Par conséquent, on peut aborder le monde du travail avec une connaissance partielle. Je ne me suis pas retrouvé démuni en commençant à travailler, c'est l'essentiel.

Test

Failed

Cette fois-ci, le constat est très différent. Le test ? Comme il constitue la partie droite du "V", il a bien fallu m'en parler. J'ai aussi suivi un cours sur le sujet, une bonne partie étant consacrée au domaine spécifique de la modélisation. "Plan de test", "test unitaire", "test automatisé", autant de termes dont on ne m'a pas parlé (ou alors pas assez, car je ne m'en souviens pas).

Lors d'un stage, j'avais réalisé un site web qui impliquait des transactions bancaires. C'était un projet compliqué que j'avais accompli sans aide et dont j'étais assez fier. Plus tard, lors d'un entretien pour un autre stage, mon interlocuteur m'a demandé comment ce site avait été testé. J'ai répondu d'un laconique "avec du bon sens", façon d'indiquer que rien n'avait été fait. En cet instant, non seulement je réalisais que mon précédent projet n'avait pas été testé, mais en plus j'ignorais complètement ce qu'il aurait fallu faire pour cela. A Bac+4, je savais déjà programmer mais je n'avais aucune idée de la façon de valider le fruit de mon travail.

Puis j'ai entamé mon stage de dernière année, dont le sujet était... le test. Je ne savais pas en quoi ça consistait. Des personnes autour de moi savaient bien de quoi il s'agissait, mais il n'y avait pas de testeur à part entière dans mon équipe. Il a fallu apprendre.

Qu'en penser ?

Question

Mon cursus n'a rien d'atypique. La plupart de mes confrères ont également suivi des études d'informatique plus ou moins généralistes, et le test n'est qu'une matière secondaire.

De prime abord, on peut évidemment déplorer la place marginale que le test occupe souvent. Même si les entreprises ont avant tout besoin développeurs, elles doivent également valider leurs logiciels. Le professionnalisme est souhaitable dans un domaine comme dans l'autre.

Cependant, je vois un signe positif dans cet apparent délaissement. Selon moi, pour bien tester, il faut d'abord savoir programmer. A l'exception des tests métier, tester nécessite une bonne compréhension de l'informatique en général. On pourrait ainsi considérer que les études classiques ne constituent qu'une introduction à la formation de testeur. Certes cette vision idyllique ne saurait conclure valablement le débat, mais pourquoi ne pas finir ce post sur une note positive ?

jeudi 3 décembre 2009

Le test qui ne pouvait échouer

Bien souvent, on s'attend à ce que les tests passent. Notre outil nous présente un rassurant "Tous les tests sont passés avec succès" ou quelque chose du genre. Idéal une veille de livraison.

Quid d'un test qui nous ferait toujours ce plaisir ?

Testons une méthode de vérification de PIN. verifyPIN retourne true lorsque le PIN de 4 chiffres passé en paramètre est correct, false sinon. Un test négatif est recommandé, qu'en dites-vous ? Dans cette version simplifiée, on prend la bonne valeur du PIN. On en modifie l'un des chiffres, puis on soumet cette valeur altérée à verifyPIN en s'attendant à ce qu'elle retourne false. On teste chaque position du PIN (le premier chiffre, le second, etc.) afin de s'assurer que chacune est impliquée dans la vérification :

// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
  // On récupère le vrai PIN
  pin = getPIN();
  // On le modifie
  pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
  // A partir de maintenant, pin n'est plus correct

  // L'authentication devrait échouer
  assertFalse(verifyPIN(pin));
}

Le test passe. Bonne nouvelle pour la livraison de demain. En fait, ce n'est pas une bonne nouvelle, par la combinaison de deux hasards :

  • Pour une raison ou pour une autre, getPIN ne retourne pas la bonne valeur du PIN. Un problème dans la configuration du test peut-être ?
  • Il se trouve que verifyPIN est buggué. verifyPIN ne vérifie pas le dernier chiffre du PIN (une faute de frappe dans la longueur de la comparaison ?). Par conséquent, si le PIN est "1234", verifyPIN accepte naturellement "1234", mais aussi "1235" et "1239". Ouch.

Ce bug est critique et pourtant il n'est pas détecté par notre test qui est pourtant conçu pour cela. Que s'est-il passé ? Le but du test est de soumettre un PIN avec un seul chiffre incorrect. Comme getPIN ne retourne pas la bonne valeur, il y a toujours des erreurs sur les trois premiers chiffres que verifyPIN détecte, peu importe que quatrième. Ainsi que le test l'attend, l'authentification échoue, mais pas pour la bonne raison.

Le bug ne sera pas détecté et le produit sera livré demain comme prévu. Un jour, quelqu'un se rendra compte de l'erreur et là...

Donnons-lui une chance d'échouer

Il existe plusieurs moyens de rendre un test résistant. Pour un test négatif de ce genre, une méthode classique est d'ajouter un cas positif :

pin = getPIN();
// Avec le bon PIN, l'authentification doit réussir
assertTrue(verifyPIN(pin));

// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
  ... // Pas d'autre changement
}

Le test est plus sûr. Désormais, lorsque getPIN retourne une mauvaise valeur, le test échoue immédiatement puisque verifyPIN indique d'emblée que le PIN est incorrect. Certes, le test sortira failed pour une mauvaise raison (une bonne raison étant un bug de code), mais au moins l'erreur ne passera pas inaperçue et le testeur fera le nécessaire pour la corriger. Au pire, on livrera après-demain.

Comment ça "paranoïaque" ?

La vérification du PIN en début de test réduit considérablement le risque évoqué ci-dessus. Toute personne raisonnable s'arrêterait là.

Cependant, "raisonnable" est tout juste un minimum pour nous testeurs. D'un côté, nous traquons les bugs, sortons des sentiers battus pour trouver les scénarios litigieux, cherchons les failles... De l'autre, nous écrivons des tests et prenons le risque de commettre exactement les même erreurs que celles que nous recherchons. Et dans notre cas, personne n'est là pour tester notre travail. Nous devons nous efforcer de trouver nos propres erreurs. Pas simple.

Pour reprendre l'exemple du PIN, on peut rendre le test encore plus sûr :

// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
  // On récupère le vrai PIN
  pin = getPIN();

  // Avec le bon PIN, l'authentification doit réussir
  assertTrue(verifyPIN(pin));

  // On le modifie
  pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
  // A partir de maintenant, pin n'est plus correct

  // L'authentication devrait échouer
  assertFalse(verifyPIN(pin));
}

La valeur de getPIN est maintenant vérifiée à chaque itération. En quoi est-ce plus sûr ? A moins d'une erreur de ma part dans le code qui précède, cela n'apporte rien. Mais nous ne cherchons pas à éviter les erreurs que nous connaissons : lorsqu'on a conscience d'une erreur, on la corrige, tout simplement. Il s'agit plutôt d'anticiper des problèmes typiques. Comme dans cet exemple :

// On récupère le vrai PIN
pin = getPIN();

// Testons toutes les positions d'un PIN à 4 chiffres
for (int i = 0; i < 4; i++) {
  // Avec le bon PIN, l'authentification doit réussir
  assertTrue(verifyPIN(pin));

  // On le modifie
  pin[i] = (pin[i] + 1) % 10; // 0 -> 1, 1 -> 2, ..., 9 -> 0
  // A partir de maintenant, pin n'est plus correct

  // L'authentication devrait échouer
  assertFalse(verifyPIN(pin));
}

L'appel à getPIN est maintenant sorti de la boucle for. Après tout, il n'est pas utile de l'appeler quatre fois n'est-ce pas ? En réalité, oui, c'est indispensable.

Le test veut soumettre à verifyPIN un PIN dont tous les chiffres sont corrects sauf un. Or, en l'état, ce n'est pas ce que fait le test. En effet, puisque pin n'est plus réinitialisé en début de boucle for, les irrégularités introduites dans pin s'accumulent d'une itération à l'autre. A la quatrième itération, ce n'est pas seulement le dernier mais tous les chiffres de pin qui sont incorrects. Une fois de plus, le bug ne serait pas découvert... si la vérification de pin n'était pas faite en début de boucle. Grâce à elle, lors de la seconde itération, le test échouera en constatant que verifyPIN(pin) retourne false.

Les self-tests ne sont pas toujours facile à introduire. On pourrait également s'interroger sur les éventuels effets de bord introduits par les appels supplémentaires à verifyPIN. C'est néanmoins une technique à garder à l'esprit, spécialement dans le cadre de tests négatifs.

dimanche 29 novembre 2009

Comment passer l'examen CFTL/ISTQB niveau Fondation ?

ISTQB Certified TesterLe CFTL permet de passer l'examen pour obtenir la certification ISTQB, niveau Fondation. Cette certification est de plus en plus populaire en France et dans le monde. De quoi s'agit-il et comment s'y prendre ?

L'examen

L'examen porte sur le Foundation Level Syllabus, soit le Syllabus Niveau Fondation, traduit en français par le CFTL (avec une version de retard à ce que je comprends, bien que cela ne fasse pas grande différence). Ce document couvre l'essentiel du test logiciel : intérêt du test, place du test dans le cycle de vie, techniques de test, etc.

L'inscription se fait en ligne sur le site du CFTL.

L'épreuve est un QCM de 40 questions. Chaque question s'accompagne de 4 réponses dont une seule est correcte. Il n'y a pas de pénalité : une mauvaise réponse ne retire pas de point. Pour passer l'examen, il faut au minimum 65% de réponses correctes, soit 26 bonnes réponses.

L'examen dure une heure. C'est une durée tout à fait suffisante. Les questions sont relativement simples : soit on sait, soit on ne sait pas. Quelques difficultés de formulation ou de petits exercices peuvent demander un peu de temps, mais globalement toutes les questions trouveront réponse rapidement. Ainsi on remplit le questionnaire en 45 minutes environ, tandis que les 15 minutes restantes sont bien utiles pour se relire et revenir sur les questions délicates.

Pendant l'épreuve, les documents sont interdits, de même que les brouillons. Si vous devez noter quelque chose, vous pouvez griffonner sur le questionnaire lui-même. Vos commentaires seront de toute façon ignorés, seules les croix comptent.

Les résultats sont communiqués par mail sous une semaine. Par la suite, le candidat reçoit un justificatif par courrier.

Préparer l'examen

Il existe plusieurs options pour se préparer à l'examen. A vous de trouver celle qui vous convient.

Aucune préparation !

Se présenter à l'examen les mains dans les poches, après tout pourquoi pas ? Le niveau Fondation couvre les bases du test logiciel. Lorsqu'on a plusieurs d'années d'expérience, on peut estimer posséder le bagage nécessaire.

Bien que cette approche soit raisonnable, je la déconseille. Même si vous avez la connaissance pour vous lancer, je vous recommande d'éviter deux difficultés :

  • Vocabulaire : Ça n'a l'air de rien, mais il est facile de se laisser surprendre par un point de vocabulaire. L'ISTQB définit un glossaire dont on retrouve les termes dans le questionnaire. Il est donc utile de consulter le glossaire français afin d'éviter les surprises.
  • Questionnaire : La forme d'un QCM peut laisser penser que l'examen est plus facile que si les réponses avaient été libres. C'est en partie vrai, et cependant les différents choix peuvent laisser perplexes. On a parfois l'impression que toutes les réponses sont bonnes, alors qu'une seule est correcte. En réalité la formulation de la question permet toujours d'isoler la bonne réponse, encore faut-il acquérir certains réflexes. Le plus simple est de se familiariser en effectuant un examen blanc. Knowledge Department en propose un d'une vingtaine de questions. On trouve aussi des examens en anglais.

Au delà de la préparation, l'examen blanc vous permettra de déterminer si vous pouvez vraiment vous présenter à l'examen sans apprentissage ou si quelques révisions s'imposent.

Coût de cette approche : 250€, soit le prix de l'examen.

Effectuer une formation puis passer l'examen

Des sociétés accréditées proposent des formations de 3 jours. Ces formations se focalisent sur le syllabus du CFTL, de sorte qu'elles préparent véritablement au passage de l'examen. C'est en fait le package "formation + examen" qui est proposé : les auditeurs passent l'examen aussitôt la formation terminée. Tant mieux, autant battre le fer tant qu'il est chaud. Par contre, j'ignore si c'est systématiquement ainsi que cela se passe. Vous devriez donc vous renseigner.

Le site du CFTL recense les sociétés qui dispensent ces formations.

N'ayant pas assisté à une telle formation, je ne peux pas les commenter d'avantage. Si vous-même vous avez cette expérience, je vous invite à laisser un commentaire pour donner vos impressions.

Coût de cette approche : environ 1500€, selon la formation.

Auto-formation

Moins cher qu'une formation et plus prudent que la simple confiance en soi, il est possible de se préparer soi-même à l'examen.

On peut opter pour le syllabus publié par le CFTL. Après tout c'est sur ce document que porte l'examen, impossible de se tromper. Malheureusement ce document est un peu austère. Le professionnel pressé saura y trouver les informations dont il a besoin pour se présenter à l'examen en toute quiétude. Le débutant risque en revanche d'avoir plus de mal à aborder ce document. Jetez-y un coup d'œil et voyez si c'est la formule qui vous convient ou pas.

Autre possibilité, il existe plusieurs ouvrages en anglais qui traitent du syllabus ISTQB niveau Fondation. J'ai opté pour Foundations of Software Testing: ISTQB Certification (vous trouverez sur Amazon.com commentaires et critiques des lecteurs, mais pour commander passez par Amazon.fr... ou par un autre site). Ce livre est très bien :

  • Véritable préparation à l'examen : Comme promis sur la couverture, le livre suit le syllabus afin que le lecteur se familiarise avec ce qu'il va rencontrer lors de l'examen. Chaque chapitre s'achève par un QCM de quelques questions, et le dernier chapitre, qui traite de l'examen lui-même, comporte un QCM complet de 40 questions. Ainsi, en répondant à toutes les questions, on effectue l'équivalent de deux examens blancs.
  • Pas trop de blabla : C'est toujours un plaisir de lire, mais autant aller à l'essentiel. Le livre compte 200 pages de contenu, auxquelles s'ajoutent le glossaire, les réponses aux QCM, etc. C'est le bon volume.
  • Agréable à lire : Cet ouvrage se veut comme un livre à lire, et non une énumération rébarbative. En fait, il constitue une bonne introduction au test logiciel, ISTQB ou pas. On pourra donc s'y plonger sans que ça soit une corvée.

Il y a évidemment un risque à se former en anglais tandis que l'examen est en français. Il serait malheureux de se laisser attraper par une traduction improbable. Le glossaire du CFTL est bilingue, ce qui permet de repérer les traductions délicates. A vrai dire, le parcours en diagonale auquel je me suis livré n'a pas révélé de piège : "regression testing" devient bien "test de régression", etc. Pas de piège. Cela s'est vérifié lors de l'examen.

Quid d'un ouvrage français pour se préparer ? Il existe bien un livre prometteur, Pratique des tests logiciels : Concevoir et mettre en oeuvre une stratégie de tests - Préparation à la certification ISTQB. Malheureusement, il est difficile de déterminer ce qu'il apporte vraiment par rapport à la certification ISTQB. On parle simplement de "préparer au passage de la certification ISTQB du métier de testeur". L'ouvrage se focalise-t-il sur cet objectif ? Ou s'agit-il simplement d'une caractéristique dont peut se prévaloir tout texte traitant du test logiciel ? Le niveau visé n'est même pas cité : Fondation ? Avancé ? En l'absence de détails, j'ai préféré m'abstenir. Si vous avez lu ce livre, publiez un commentaire, ici ou ailleurs, afin d'éclairer les lecteurs hésitants !

Coût de cette approche :

  • Un livre : 30€ environ
  • L'examen : 250€

Bonne certification !

Un débutant trouvera avantage à passer l'examen CFTL/ISTQB niveau Fondation. Il couvre l'essentiel du test logiciel. Pour le testeur expérimenté, l'examen correspond plus à une validation d'acquis qu'à un véritable apprentissage. C'est un bon point de départ pour faire reconnaitre son expérience.

Quel que soit votre niveau et la façon dont vous préparerez l'examen, je vous souhaite bonne chance !