Développement d'essai avec FitNesse

Au cours des dernières années, j'ai travaillé dans tous les rôles du processus de test, en utilisant JavaScript côté serveur, Perl, PHP, Struts, Swing et des architectures basées sur des modèles. Tous les projets différaient, mais ils avaient tous des points communs: les délais étaient tardifs et les projets avaient des difficultés à réaliser ce dont le client avait vraiment besoin.

Chaque projet avait une sorte d'exigence, certains étaient très détaillés, certains, seulement quelques pages. Ces exigences se déroulaient généralement en trois phases:

  • Ils ont été écrits (soit par le client ou par l'entrepreneur) et ont reçu une sorte d'acceptation officielle
  • Les testeurs ont essayé de travailler avec les exigences et les ont trouvées plus ou moins inadéquates
  • Le projet est entré dans une phase de test d'acceptation, et le client s'est soudainement souvenu de toutes sortes de choses que le logiciel devait faire en plus / différemment

La dernière phase a conduit à des changements, ce qui a conduit à des délais manqués, ce qui a mis le stress sur les développeurs, ce qui a entraîné davantage d'erreurs. Le nombre de bogues a commencé à augmenter rapidement et la qualité globale du système a décliné. Semble familier?

Examinons ce qui n'a pas fonctionné dans les projets décrits ci-dessus: le client, le développeur et le testeur n'ont pas travaillé ensemble; ils ont transmis les exigences, mais chaque rôle avait des besoins différents. De plus, les développeurs développaient généralement des types de tests automatisés, et les testeurs ont également essayé d'automatiser certains tests. Habituellement, ils ne pouvaient pas coordonner suffisamment les tests, et de nombreux éléments ont été testés deux fois, tandis que d'autres (généralement les parties dures), pas du tout. Et le client n'a vu aucun test. Cet article décrit un moyen de résoudre ces problèmes en combinant des exigences avec des tests automatisés.

Entrez FitNesse

FitNesse est un wiki avec quelques fonctions supplémentaires pour déclencher des tests JUnit. Si ces tests sont combinés avec des exigences, ils servent d'exemples concrets, ce qui rend les exigences encore plus claires. De plus, les données de test sont organisées de manière logique. La chose la plus importante dans l'utilisation de FitNesse, cependant, est l' idée sous-jacente, ce qui signifie que les exigences se révèlent être écrites (en partie) sous forme de tests, ce qui les rend testables et, par conséquent, leur satisfaction vérifiable.

En utilisant FitNesse, le processus de développement pourrait ressembler à ceci: L'ingénieur des exigences écrit les exigences dans FitNesse (au lieu de Word). Il essaie d'impliquer le client autant que possible, mais cela n'est généralement pas réalisable au quotidien. Le testeur jette un œil au document à plusieurs reprises et pose des questions difficiles dès le premier jour. Parce que le testeur pense différemment, il ne pense pas: "Que fera le logiciel?" mais "Qu'est-ce qui pourrait mal tourner? Comment puis-je le casser?" Le développeur pense plus comme l'ingénieur des exigences; il veut savoir: "Que doit faire le logiciel?"

Le testeur commence à écrire ses tests tôt, lorsque les exigences ne sont même pas encore terminées. Et il les écrit dans les exigences. Les tests font partie non seulement des exigences, mais également du processus d'examen / d'acceptation des exigences, ce qui présente des avantages importants:

  • Le client pense également aux tests. Habituellement, elle s'implique même dans leur création (vous pourriez être surpris de voir à quel point elle peut s'amuser avec cela).
  • La spécification devient beaucoup plus détaillée et précise, car les tests sont généralement plus précis qu'un simple texte.
  • En réfléchissant tôt à des scénarios réels, en fournissant des données de test et en calculant des exemples, vous obtenez une vision beaucoup plus claire du logiciel, comme un prototype, mais avec plus de fonctions.

Enfin, les exigences sont transmises au développeur. Il a un travail plus facile maintenant, car la spécification est moins susceptible de changer et à cause de tous les exemples inclus. Voyons comment ce processus facilite le travail d'un développeur.

Mettre en œuvre le test d'abord

Habituellement, la partie la plus difficile du démarrage du développement test-first est que personne ne veut passer autant de temps à écrire des tests, seulement ensuite pour trouver un moyen de les faire fonctionner. En utilisant le processus décrit ci-dessus, le développeur reçoit les tests fonctionnels dans le cadre de son contrat. Ses tâches passent de «Construisez ce que je veux et vous avez terminé, jusqu'à ce que j'examine votre travail et apporte des modifications» à «Faites fonctionner ces tests et vous avez terminé». Désormais, tout le monde a une meilleure idée de ce qu'il faut faire, du moment où le travail doit être achevé et de la position du projet.

Tous ces tests ne seront pas automatisés et tous ne seront pas des tests unitaires. Nous divisons généralement les tests dans les catégories suivantes (les détails suivront):

  • Tests basés sur les données qui doivent être implémentés en tant que tests unitaires. Les calculs en sont l'exemple type.
  • Tests basés sur des mots-clés qui automatisent l'utilisation des applications. Ce sont des tests système et nécessitent l'exécution de l'application. Les boutons sont cliqués, les données sont saisies et les pages / écrans résultants sont vérifiés pour contenir certaines valeurs. L'équipe de test implémente généralement ces tests, mais certains développeurs en bénéficient également.
  • Tests manuels. Ces tests sont soit trop coûteux à automatiser et les erreurs éventuelles pas assez graves, soit ils sont tellement fondamentaux (page de démarrage non affichée) que leur casse serait immédiatement découverte.

Quand j'ai lu pour la première fois sur FitNesse en 2004, j'ai ri et j'ai dit que cela ne fonctionnerait jamais. L'idée d'écrire mes tests dans un wiki qui les transforme automatiquement en tests me paraissait trop absurde. Il s'est avéré que j'avais tort. FitNesse est vraiment aussi simple qu'il y paraît.

Cette simplicité commence avec l'installation. Téléchargez simplement la distribution complète de FitNesse et décompressez-la. Dans la discussion suivante, je suppose que vous avez décompressé la distribution dans C: \ fitnesse.

Démarrez FitNesse en exécutant le script run.bat( run.shsous Linux) dans C: \ fitnesse. Par défaut, FitNesse exécute un serveur Web sur le port 80, mais vous pouvez spécifier un port différent, disons 81, en l'ajoutant -p 81à la première ligne du fichier de commandes. C'est tout ce qu'on peut en dire; vous pouvez maintenant accéder à FitNesse à l'adresse // localhost: 81.

Dans cet article, j'utilise la version Java de FitNesse sous Windows. Cependant, les exemples peuvent également être utilisés pour d'autres versions (Python, .Net) et plates-formes.

Quelques tests

La documentation en ligne de FitNesse fournit quelques exemples simples (comparables au tristement célèbre exemple d'argent de JUnit) pour vous aider à démarrer. Ils sont parfaits pour apprendre à utiliser FitNesse, mais ils ne sont pas suffisamment compliqués pour convaincre certains sceptiques. Par conséquent, je vais utiliser un exemple concret de l'un de mes projets récents. J'ai considérablement simplifié le problème et le code, non tiré directement du projet, a été écrit à des fins d'illustration. Pourtant, cet exemple devrait être suffisamment compliqué pour démontrer la puissance de la simplicité de FitNesse.

Supposons que nous travaillons sur un projet qui implémente un logiciel d'entreprise Java complexe pour une grande compagnie d'assurance. Le produit gérera l'ensemble des activités de l'entreprise, y compris la gestion des clients et des contrats et les paiements. Pour notre exemple, nous allons regarder une infime partie de cette application.

En Suisse, les parents ont droit à une allocation pour enfant par enfant. Ils ne reçoivent cette allocation que si certaines circonstances sont réunies et le montant varie. Ce qui suit est une version simplifiée de cette exigence. Nous allons commencer par les exigences «traditionnelles» et les transférer ensuite vers FitNesse.

Plusieurs phases d'allocations familiales existent. La demande commence le premier jour du mois de naissance de l'enfant et se termine le dernier jour du mois où l'enfant atteint la limite d'âge, termine son apprentissage ou décède.

À l'âge de 12 ans, la créance est portée à 190 CHF (symbole monétaire officiel de la Suisse) à compter du premier jour du mois de naissance.

L'emploi à temps plein et à temps partiel des parents entraîne des demandes différentes, comme le montre la figure 1.

Le taux d'emploi actuel est calculé sur les contrats de travail actifs. Le contrat doit être valide et si une date de fin est définie, il doit être situé dans la «période d'activation». La figure 2 montre à combien d'argent un parent a droit, selon l'âge de l'enfant.

La réglementation régissant ces paiements est adaptée tous les deux ans.

En première lecture, la spécification peut sembler exacte et un développeur devrait être en mesure de la mettre en œuvre facilement. Mais sommes-nous vraiment sûrs des conditions aux limites? Comment testerions-nous ces exigences?

Conditions aux limites
Boundary conditions are the situations directly on, above, and beneath the edges of input and output equivalence classes. Experiences show that test cases exploring boundary conditions have a higher payoff than test cases that do not. A typical example is the infamous "one-off" on loops and arrays.

Scenarios can be a great help in finding exceptions and boundary conditions, as they provide a good way to get domain experts to talk about business.

Scenarios

For most projects, the requirements engineer hands the specification to the developer, who studies the requirements, asks some questions, and starts to design/code/test. Afterwards, the developer hands the software to the test team and, after some rework and fixes, passes it on to the customer (who will likely think of some exceptions requiring changes). Moving the text to FitNesse won't change this process; however, adding examples, scenarios, and tests will.

Scenarios are especially helpful for getting the ball rolling during testing. Some examples follow. Answering the question of how much child allowance is to be paid to each will clarify a lot:

  • Maria is a single parent. She has two sons (Bob, 2, and Peter, 15) and works part-time (20 hours per week) as a secretary.
  • Maria loses her job. Later, she works 10 hours per week as a shop assistant and another 5 hours as a babysitter.
  • Paul and Lara have a daughter (Lisa, 17) who is physically challenged and a son (Frank, 18) who is still in university.

Just talking through these scenarios should help the testing process. Executing them manually on the software will almost certainly find some loose ends. Think we can't do that, since we don't have a prototype yet? Why not?

Keyword-driven testing

Keyword-driven testing can be used to simulate a prototype. FitNesse allows us to define keyword-driven tests (see "Totally Data-Driven Automated Testing" for details). Even with no software to (automatically) execute the tests against, keyword-driven tests will help a lot.

Figure 3 shows what a keyword-driven test might look like. The first column represents keywords from FitNesse. The second column represents methods in a Java class (we write those, and they need to follow the restrictions put on method names in Java). The third column represents data entered into the method from the second column. The last row demonstrates what a failed test might look like (passed tests are green). As you can see, it is quite easy to find out what went wrong.

Such tests are easy and even fun to create. Testers without programming skills can create them, and the customer can read them (after a short introduction).

Defining tests this way, right next to the requirement, has some important advantages over the traditional definition of test cases, even without automation:

  • The context is at hand. The test case itself can be written with the least possible amount of work and is still precise.
  • If the requirement changes, there is a strong chance that the test will change as well (not very likely when several tools are used).
  • The test can be executed at once to show what needs to be fixed to make this new/changed requirement work.

To automate the test, a thin layer of software is created, which is delegated to the real test code. These tests are especially useful for automating manual GUI tests. I developed a test framework based on HTTPUnit for automating the testing of Webpages.

Here is the code automatically executed by FitNesse:

package stephanwiesner.javaworld;

import fit.ColumnFixture;

public class ChildAllowanceFixture extends ColumnFixture { public void personButton() { System.out.println("pressing person button"); } public void securityNumber(int number) { System.out.println("entering securityNumber " + number); } public int childAllowance() { System.out.println("calculating child allowance"); return 190; } [...] }

The output of the tests can be examined in FitNesse as well (see Figure 4), which greatly helps with debugging. In contrast to JUnit, where one is discouraged from writing debug messages, I find them absolutely necessary when working with automated Web tests.

When testing a Web-based application, error pages are included in the FitNesse page and displayed, making debugging much easier than working with log files.

Data-driven testing

Alors que les tests basés sur les mots clés conviennent à l'automatisation de l'interface graphique, les tests basés sur les données sont le premier choix pour tester le code qui effectue tout type de calcul. Si vous avez déjà écrit des tests unitaires, quelle est la chose la plus ennuyeuse à propos de ces tests? Il y a de fortes chances que vous pensiez aux données. Vos tests seront pleins de données, qui changent souvent, faisant de la maintenance un cauchemar. Tester différentes combinaisons nécessite des données différentes, ce qui rend probablement vos tests compliqués et horribles.