Préambule
Nous sommes en train de reprendre un module pour préparer une bascule prochaine. Ce module concerne une gestion de listes de diffusion (de discussions, distributions, newsletters, ...) qui contient des actions simples : création ou suppression d'une liste, ajout ou suppression d'un abonnement.
C'est l'occasion d' implémenter des tests sur ce module. Pas tout à fait TDD dans le principe, mais cela aura au moins le mérite de commencer à prendre la main sur les frameworks et la méthode d'implémentation.
On va procéder par étapes, en commençant petit, il est toujours préférable d'être raisonnable dans ses objectifs, think big, start small disait mon grand-père agile.
Dans l'écriture de ces tests, il va s'agir de vérifier les entrées (chaînes de caractères) passées aux différentes méthodes, à savoir que le nom de la liste, et l'email de l'abonné soient bien formés (en gros que les regexp qui vérifient ces entrées fonctionnent).
On tentera (presque) de respecter le modèle du triptyque RED-GREEN-REFACTOR, sauf que dans notre cas, il existe du code :
- écriture d'un test, le faire planter, le résultat du test runner doit être au rouge,
- faire passer le test au vert,
- réécrire le code le cas échéant
Outils
Le framework de tests utilisé est MbUnit, et le runner un add-in VStudio TestDriven.NET. Une version d'évaluation de ReSharper est également installée pour accélérer les ajustements de code.
Dès que je peux, je tenterai également ce projet qui me parait fort intéressant : mbunit-R#.
Mise en place
Un peu de théorie
Le module s'appelle ListManager, notre projet de tests se nommera donc selon la convention ListManager.Tests. Les noms des méthodes de tests sont écrites selon le format suivant : ''NomMethodManager_ResultatAttendu_ScenariodeTest'.
Les méthodes de notre Manager à tester sont de l'ordre de 4, on s'arrêtera à la 1ère pour notre exemple :
- CreateList(list) : créer une liste
- DeleteList(list) : supprimer une liste
- Subscribe(list, email) : abonner à une liste un email
- Unsubscribe(list, email) : désabonner d'une liste un email
Sur notre existant, chaque méthode ne renvoie que des exceptions, notamment sur la vérification de la validité des entrées, ici list et email. Des expressions régulières sont utilisées pour tester le format de ces 2 paramètres. En gros que les caractères du type [a-z0-9-_.] sont acceptés.
Chaque méthode renvoie une exception ArgumentException s'il s'avère que le format des chaînes est erroné.
Le Manager peut être utilisé avec 2 fournisseurs possibles, qui représentent 2 moteurs de listes de diffusion (SYMPA ou LYRIS). L'un ou l'autre est sélectionné selon la stratégie adoptée. Les tests de différents cas d'entrées sont à dérouler bien entendu sur les 2 providers.
La base posée, passons à l'écriture de nos tests.
Passons à la pratique
Testons CreateList(). Le résultat attendu est une exception ArgumentException sur des chaînes qui ne respectent pas le format attendu. Le test doit être au vert si la méthode renvoie donc bien une exception sur une mauvaise entrée, par exemple :
CreateList("éléphant") doit renvoyer une erreur. Le code pour tester que ces cas renverront bien un exception pour les 2 providers, l'attribut [ExpectedArgumentException] de MbUnit permet de tester l'exception attendue :
using ListManagerConnector;
using MbUnit.Framework;
namespace ListManager.Tests
{
[TestFixture]
public class ListManagerTests
{
[Test]
[ExpectedArgumentException]
public void CreateList_onSympa_ShouldThrow_IfNameOfList_IsNotWellFormed()
{
var lm = AbstractFactory.Factory("SYMPA").ListManagerControler;
lm.CreateList("éléphant");
}
[Test]
[ExpectedArgumentException]
public void CreateList_onLyris_ShouldThrow_IfNameOfList_IsNotWellFormed()
{
var lm = AbstractFactory.Factory("LYRIS").ListManagerControler;
lm.CreateList("éléphant");
}
}
}
Ce premier est pas mal, mais rien que pour éléphant, il nous faut 2 méthodes de tests. Pour chaque motif à tester, 2 méthodes supplémentaires, ce qui devient vite rébarbatif et donc peu motivant. Pour améliorer cela, MbUnit fournit la possibilité d'injecter des jeux de tests dans les méthodes (dans le même principe que des tests fonctionnels, voir FitNesse). Pour cela, utilisons les attributs [RowTest] (qui remplace l'attribut de base [Test]) et [Row] pour dérouler les cas intéressants et ce, sur les 2 providers.
Testons les motifs éléphant, l'éléphant, et certains autres cas sur les providers nommés SYMPA et LYRIS. Le paramètre strategy permet de tester chaque méthode sur les 2 providers :
using ListManagerConnector;
using MbUnit.Framework;
namespace ListManager.Tests
{
[TestFixture]
public class ListManagerTests
{
[RowTest]
[Row("malist@cc", "SYMPA")]
[Row("malist@dummy.fr", "SYMPA")]
[Row("", "SYMPA")]
[Row("éléphant", "SYMPA")]
[Row("l'elephant", "SYMPA")]
[Row("malist@lyris.fr", "LYRIS")]
[Row("malist@fr", "LYRIS")]
[Row("", "LYRIS")]
[Row("éléphant", "LYRIS")]
[Row("l'elephant", "LYRIS")]
[ExpectedArgumentException]
public void CreateList_ShouldThrow_IfNameOfList_IsNotWellFormed(string listname, string strategy)
{
var lm = AbstractFactory.Factory(strategy).ListManagerControler;
lm.CreateList(listname);
}
}
}
et voilà, comment en une méthode implémenter un jeu de 10 tests. Jouons la méthode avec TestDriven sous VStudio 2008 et le runner de MbUnit. On voit que MbUnit déroule chaque cas (éléphant, l'éléphant, ..) sur la même méthode.
Sous Visual Studio :

En lançant le runner de MbUnit :

Si nous avions introduit une donnée correcte, un des tests aurait été dans l'état rouge ou FAILURE, par exemple le test suivant doit échouer :
[Row("od", "SYMPA")]

à chaque nouvelle écriture d'une méthode de test, on commencera par la faire échouer de cette façon.
Rétrospective
Ces premiers tests répondent à tout ou partie à notre besoin : vérifier que les entrées de nos méthodes sont bien formées.
Quelques retours sur ces 1ers tests :
- dans notre cas, il a fallu que les 2 serveurs de listes soient en fonction, sans compter la configuration ad-hoc nécessaire. Aussi, il aurait été bénéfique que les 2 providers soient injectés dans le manager, afin de pouvoir les simuler lors des tests, part des Stubs / Mocks par exemple. Ici, on part du principe que nos serveurs fonctionnent et rendent le service attendu. Bien sûr, si l'objectif était de les tester alors on déploierait les tests nécessaires, même si cela concerne plus de l'intégration que des tests unitaires,
- on comprend mieux la nécessité d'une usine d'intégration continue. Je m'imagine mal exécuter des centaines de tests à chaque fois sur mon IDE. L'usine peut apporter une solution à cette question.
- j'ai pu m'apercevoir que des cas passaient alors qu'ils sont interdits, des bugs donc éventuels, maintenant corrigés.
- au niveau de la conception objet, on s'aperçoit vite que c'est perfectible, que le code doit être non seulement OO mais aussi testable, ce qui implique un compromis des deux.
- bonne expérience, à répéter de toute urgence, passionnant !
Tips & Tricks
Sous Visual Studio, à l'aide de TestDriven, le fait de se placer sur une méthode de test et de lancer les tests ne déroule ces derniers que sur la méthode.
ReSharper ou R# est un très très bon add-in, c'est assez remarquable ce qu'il arrive à conseiller comme réécriture du code, il reste à convaincre d'investir 200 € (par licence).