Introduction au test logiciel/Problématiques de tests

Ce chapitre vise à décrire des approches pour résoudre des problématiques de tests récurrentes et présente des outils complémentaires qui répondent à ces problématiques spécifiques.

Tester des applications concurrentes

modifier

Faire appel à la concurrence dans une application peut être nécessaire, souvent pour des raisons de performance. Toutefois, la multiplication des processus, des threads ou des fibres peut créer des bugs (interblocage, famine…) car il est difficile de bien synchroniser l'application.

De plus, l'ordonnancement des tâches par les systèmes d'exploitation multi-tâches ou sur les systèmes parallèles (processeurs multi-cœurs ou systèmes multi-processeurs) n'est pas toujours le même à chaque exécution de l'application ou des applications, ce qui complique les tests.

Tout d'abord, il convient de mener une analyse statique du code de l'application, cela devrait éliminer les erreurs fréquentes.

Java
Concutest (alias ConcJUnit)

Tester des applications distribuées

modifier

Qu'il s'agisse d'applications client-serveurs, d'une application en architecture n-tiers, ou d'un système distribué en grande échelle (tel qu'un système pair-à-pair) : ces systèmes sont intrinsèquement concurrents. En plus de l'aspect distribué, ils posent donc aussi bien la problématique du test de système concurrent.

Une des solutions consiste, via un outil spécialisé, à faire intervenir, pour tous les nœuds un seul et même TestRunner centralisé faisant appel aux différents tests les uns après les autres, en attendant que chaque machine soit synchrone par rapport au déroulement du test général. Cette synchronisation centralisée lève l'indéterminisme induit par la distribution et permet de tester normalement. C'est l'approche retenue par deux outils :

JBoss Distributed Testing Tool
PeerUnit
Un projet INRIA plutôt destiné au test des systèmes pair-à-pairs.

Cette approche à l'avantage de permettre de tester un système in situ, vraiment distribué (avec les temps de latence réseau notamment). Principal inconvénient, pour automatiser le déroulement des tests, il faudra également automatiser le déploiement.

Tester une application avec des données persistantes

modifier

http://www.dbunit.org/

Les « fixtures »

modifier

Lorsque le système testé exploite une base de données, l'écriture des tests devient compliquée. En effet, si on utilise une base de données de test présente sur la machine du développeur, les données risquent de changer au cours du temps (tests manuels). De même, si on exécute plusieurs tests de suite sur une même base de données et qu'un test qui modifie les données en base échoue, on a laissé la base dans un état indéterminé, peut-être incohérent, ce qui peut invalider le reste des tests.

C'est pourquoi il faut absolument cloisonner les tests et, avant chaque test, remettre la base dans un état cohérent et déterminé. Cela peut être laborieux et cela peut aboutir à des tests qui prennent 100 lignes pour insérer des données en base (avec le risque d'erreur) puis 5 lignes pour faire effectivement le test.

C'est ici que les « fixtures » interviennent : ces fixtures sont des doublures qui vont venir peupler la base (vidée entre chaque test) pour la placer dans un état cohérent juste avant le test.

On décrit ce petit jeu de données dans un fichier lisible, souvent au format YAML (plus rarement en XML ou en JSON). Par exemple :

# Fixtures pour la table 'books'
miserables:
  title: Les misérables
  author: Victor Hugo
  year: 1862

lotr:
  title: The Lord of the Rings
  author: J. R. R. Tolkien
  year: 1954

dunces:
  title: A Confederacy of Dunces
  author: John Kennedy Toole
  year: 1980

À chaque modification du schéma de la base, on crée ou on adapte ces petits fichiers. Une API permet alors d'interpréter ces fichiers et de charger les données en base depuis les tests. Par exemple, avec le framework Play! (Java) :

@Before
public void setUp() {
    Fixtures.deleteAll();
    Fixtures.load("data.yml");
}

En l'état actuel, il faut reconnaître qu'il n'existe pas ou peu d'outils indépendants pour les fixtures. Ces derniers sont plutôt fournis avec les frameworks Web comme c'est le cas dans Play! (Java), Django (Python), Ruby on Rails ou CakePHP. Cela est bien dommage, l'usage des bases de données ne se limitant pas aux applications Web.

« Fixtures » se traduirait par « garniture », mais il semble plus pertinent d'utiliser le terme « échantillons ».

Tester les procédures embarquées d'une base de données

modifier

Pour des raisons de performances, il est possible que des traitements métiers sur les données soient réalisés en base. Ces traitements sont des codes stockés directement dans le SGBD, ils sont codés dans un langage tels que PL/SQL ou PL/pgSQL.

Plutôt que de tester ces procédures au milieu des autres tests de l'application, il est préférable de mener les tests au plus près, avec des outils insérant les données et déclenchant les procédures. De simples requêtes de lecture en base et des assertions permettent de s'assurer que les traitements effectués par les procédures stockées produisent de bons résultats.

Parmi ces outils, on peut citer SQLUnit.

Résoudre des dépendances vers des composants complexes

modifier

S'il est possible d'écrire des doublures pour des petits composants, certains composants complexes ne peuvent pas être vraiment doublés. Par exemple, une base de données ou un serveur de mail (SMTP) pour une application qui envoie des e-mails à ses utilisateur.

Dans ce cas, il faut plutôt essayer de remplacer ces composants usuels par d'autres briques plus adaptées, voire prévues pour les tests.

Bases de données

modifier

Il est fort probable que l'application a vocation à être utilisée, en production, sur une base de données client/serveur (MySQL, PostgreSQL ou autre). Cependant, on peut envisager de profiter d'une couche d'abstraction de la base de données[1] pour substituer cette base client/serveur par une base de données embarquée (comme SQLite pour PHP/Python/Ruby ou HSQLDB pour Java). Bien que ces bases de données soient moins performantes et ne gèrent souvent pas la concurrence ou d'autres fonctionnalités attendues d'une bonne base de données, elles sont très souvent amplement suffisantes pour travailler sur un petit ensemble de données de test.

Serveur de mail

modifier

L'application peut avoir besoin d'envoyer des courriels automatiques à ses utilisateurs (notifications, confirmations…). Sur leurs machines, les développeurs, lors des tests peuvent substituer au serveur de mail (SMTP) un simulacre comme Wiser pour Java ou fakemail pour Python (utilisable aussi avec SimpleTest pour PHP). Ces simulacres s'installent et se mettent à l'écoute du port 25, pour recevoir les requêtes SMTP envoyées par l'application.

Ces solutions permettent d'utiliser l'application et de provoquer l'envoi de courriels sans risquer de vraiment envoyer les courriels. Par contre, ces outils permettent de vérifier qu'un courriel a bien été envoyé. Illustrons cela à l'aide d'un extrait de la documentation de Wiser :

Wiser wiser = new Wiser();
wiser.setPort(2500); // Default is 25
wiser.start();

// Après envoi du courrier 

for (WiserMessage message : wiser.getMessages()) {
    String envelopeSender = message.getEnvelopeSender();
    String envelopeReceiver = message.getEnvelopeReceiver();
    MimeMessage mess = message.getMimeMessage();

    // il n'y a plus qu'à vérifier que ces messages ont le bon
    // destinataire, le bon contenu…
}

Système de fichiers

modifier

Pour tester des applications qui font appel au stockage de données sur le système de fichiers (partage de fichiers en réseau, logiciels de sauvegarde, de téléchargement ou de transferts de fichiers…), il peut être utile d'avoir une doublure pour simuler le système de fichiers et pouvoir faire des vérifications sur les données lues ou écrites.

Ruby
fakefs
Java
MockFtpServer
Python
gpsfake

Tester des interfaces graphiques

modifier

Le monkey testing

modifier

Un « monkey test » (littéralement test du singe) est en fait un test de fuzzing appliqué aux interfaces graphiques. Le singe va lancer l'interface, et appuyer au hasard sur des boutons, entrer des données aléatoires etc. On peut ainsi détecter les cas où une mauvaise entrée provoque une erreur.

Les développeurs Android (une plateforme pour téléphone mobile qui permet de réaliser des applications graphiques prenant en charge un écran tactile) peuvent utiliser un singe fourni avec le Kit de développement pour tester leurs applications. Le singe appuie sur des boutons, remplit les formulaires mais peut aussi simuler des glisser-déposer sur l'écran tactile.

Tester des interfaces Web

modifier

Divers outils spécialisés permettent de programmer des cas d'utilisation d'une interface Web, par exemple :

  1. Charger cette URL,
  2. Remplir un formulaire,
  3. Valider le formulaire,
  4. Vérifier que la page chargée contient bien un texte donné.

L'outil peut également proposer, plutôt que de programmer les tests, d'enregistrer la série de manipulations effectuées par un utilisateur dans un navigateur (clics, soumission de formulaires...) et de la reproduire dans les tests.

Parmi ces outils, citons :

watir
qui s'adresse plutôt au développeurs Ruby. Watij est un portage Java
Windmill
qui propose des API Python, JavaScript et Ruby
Selenium HQ
qui propose des API C#, Java, Perl, PHP, Python et Ruby
Canoo WebTest
permet de décrire les tests en XML ou en Groovy ou de les enregistrer depuis Firefox avec WebTestRecorder

Ces outils sont comparés dans l'article « List of web testing tools » de Wikipedia anglophone.

Tellurium Automated Testing Framework, JWebUnit.

Tester des interfaces lourdes

modifier

De la même façon, des outils sont spécialisés pour le test fonctionnel d'interfaces utilisateurs lourdes basées sur toolkit comme SWING (Java), par exemple.

Python
dogtail

Tester du code destiné à fonctionner dans un serveur d'application

modifier

En Java, si vous développez une application faisant appel à des composants JEE, tels qu'EJB, JPA ou autre, vous n'allez pas tester en déployant votre application dans un JBoss durant les tests. Vous pouvez essayer de remplacer ces dépendances par d'autres plus légères (comme Apache OpenEJB). Depuis Java 1.6, il doit être possible d'utiliser EJB de façon embarquée via javax.ejb.embeddable.*.

Java
Cactus, JBoss Arquillian

Tester une application Android

modifier

 

Wikipédia propose un article sur : « Android ».

L'API Android intègre déjà des éléments permettant d'écrire des tests pour les activités, ils se trouvent dans package android.test.*. Un outils de doublures est également inclus dans package android.test.mock.*. La documentation développeur intègre plusieurs chapitres consacrés au tests.

Il existe également des outils spécialisés :

Gérer du code patrimonial

modifier

Le code patrimonial (« code légué » ou « legacy code ») est une base de code souvent de mauvaise qualité, dont plus personne ou peu de personnes n'a encore la connaissance. Il s'agit de projets anciens faisant souvent appel à des technologies anciennes voire obsolètes mais qui sont toujours en production.

Les tests peuvent aider à maintenir ce genre de code. Lorsqu'on doit ainsi corriger un bogue dans une telle application et qu'il n'y a pas ou peu de tests, on peut commencer par écrire des tests, simples d'abord. On poursuit ensuite en raffinant les tests, pour se rapprocher de l'origine du bogue. On a trouvé le bogue une fois qu'on a écrit un test qui ne passe pas alors qu'il devrait. Le bogue, ainsi isolé, peut maintenant être corrigé. On peut repasser tous les tests écrits depuis le début pour vérifier qu'on n'a pas provoqué une régression en modifiant le code. On parle alors de « characterization test », des tests qui assurent que le programme fonctionne de la même manière qu'au moment où les tests ont été écrits.

En procédant ainsi pour chaque bogue découvert, on constitue une base de tests et on tend ainsi à retrouver un environnement de travail plus sain.

Références

modifier
  1. Il peut s'agir d'une simple couche d'abstraction au niveau de SQL (JDBC pour Java, PDO pour PHP…) qui permet d'interagir avec une base de données selon une API commune. Il suffit alors de changer le pilote chargé au lancement de l'application pour l'adaptée au SGBD utilisé. Un pilote pour MySQL, un autre pour SQLite, etc. : il suffit de changer le pilote pour changer de SGBD et le code reste inchangé. Une autre couche, plus haut niveau, peut permettre de s'abstraire du SGBD utilisé. Il s'agit de la couche de mapping objet-relationnel : Hibernate pour Java, SQLAlchemy pour Python, Active record pour Ruby, Doctrine pour PHP… Toutefois la plupart de ces couches de mapping reposent sur la première couche que nous avons décrite, cela reste donc une question de changement de pilote : une simple directive de configuration.