Visual T# (prononcé []) est un environnement de développement gratuit de tests unitaires intégré à Visual Studio, mais peut s'utiliser indépendamment. Il comprend :

T# : un langage de programmation destiné aux tests unitaires. T# est basé sur le langage C# v2, ce qui le rend très naturel à utiliser par les développeurs .NET. De nouveaux mots-clés y ont été introduits pour simplifier l'écriture de tests. Il met beaucoup d'emphase sur la définition de l'intention des tests ; Des outils pour compiler les tests, les exécuter, les tester et pour facilement naviguer parmi les tests, même pour un grand nombre de tests.

Avantages

T# est un langage de programmation pour Microsoft .NET, compatible avec C# v2 (sauf en ce qui concerne le code non "managed"), et offre les avantages suivants par rapport aux solutions NUnit ou Visual Studio Team Test : utilisation des bonnes pratiques : identifie bien les trois parties (préparation, exécution, vérification) ; identification rapide des problèmes : les tests mal construits sont séparés des tests qui ont échoué ; facilité d'écriture des validations : un seul mot clé assert pour traiter tous les cas ; efficacité dans le débogage : les tests concernant la déclaration que vous travaillez sont exécutés sans savoir où vous les avez déclarés ; utilisation de différents contextes : ré-exécutez mêmes les tests pour différents contextes sans les réécrire ; indication des tests logiques manquants : les tests logiques manquants sont indiqués dès la compilation du code de test ; simplicité d'écriture : chaque vérification s'effectue en 1 ligne de code (même pour les événements, les exceptions...), utilisez des vérifications relatives.

Le langage Voici un exemple d'un test minimal, écrit en T# :

testclass { test

Calculatrice calc = new Calculatrice();

runtest double somme = calc.Ajouter(1, 2);

assert somme == 3;

Bonnes pratiques pour les tests unitaires T# est complètement orienté bonnes pratiques.

Structure d'un test Un test unitaire est toujours composé de trois parties : Préparation : création des instances nécessaires à l'exécution du test Exécution : une seule instruction pour utiliser la déclaration à tester dans un contexte donné Vérification : vérification que les résultats attendus (directement ou indirectement) sont bien là. La partie la plus importante étant Exécution, c'est pour elle que vous écrivez le test.

T# identifie clairement la partie Exécution en faisant commencer l'instruction par le mot clé runtest. La préparation est donc naturellement tout ce qui se trouve avant runtest, et la vérification après runtest.

Vérifications La partie vérification s'assure que tous les effets (par exemple : retour de fonction, changements des paramètres, modification d'instance, de déclarations statiques, de fichiers, bases de données...) escompté lors de l'utilisation de la déclaration se sont bien effectués comme prévu.

T# n'offre qu'un seul mot-clé pour cela : assert. Le message est automatiquement généré à partir du code source. Ainsi, si le test précédent échoue (si la fonction Ajouter est mal codée en retournant toujours 0), le test échouera avec le message d'erreur suivant : "Expected: somme == 3 but was: 0 == 3". Il est ainsi très rapide de déterminer que la somme vaut 0, mais que l'on s'attendait à 3. Cette façon de générer le message d'erreur le rend plus proche du code source (donc plus facile de faire le lien avec le code quand l'erreur apparait) et décompose les différentes valeurs impliquées (si plusieurs variables étaient impliquées, on saurait alors la valeur de chacune des variables et non pas de l'expression finale), rendant le déboguage beaucoup plus facile.

De plus, les conversions naturelles du langage de programmation sont utilisées (somme est un double, 3 est un entier). Ainsi, pas de problème pour comparer les deux contrairement à Visual Studio Team Test pour lequel vous devez écrire : Assert.AreEqual(3.0, somme); pour faire la même vérification!

Quatre états pour un test Le test étant du code, il peut lui aussi échouer. Contrairement aux concurrents, T# sait exactement l'instruction qui teste réellement (runtest). Ainsi, il est en mesure de déterminer si l'échec du test s'effectue avant ou après cette instruction : Si le test échoue avant l'instruction runtest, c'est que l'on n'arrive pas à se placer dans le contexte voulu pour tester notre intention. Le test n'est pas bon. Si le test échoue après l'instruction runtest, c'est que le code testé ne fait probablement pas la bonne chose. Le code testé n'est pas bon.

T# a donc quatre états pour un test : Passed : le test a réussi. Ignored : le test est temporairement ignoré. Failed : le test a échoué, le code testé n'est pas bon. Invalid : le test a échoué, le contexte pour tester n'est pas bon, il est donc impossible de vérifier si le code testé est correct ou non.

Afin de profiter de cette différence et de rendre le test très clair, utilisez des assert avant l'instruction runtest :

testclass { test

Produit prod = new Produit("T#", 123);

runtest prod.Prix = 0;

assert prod.Prix == 0;

Dans cet exemple, on veut rendre gratuit T#. Le test passe. Le code set de la propriété Prix est donc correct. Vraiment? Mais si ni le constructeur, ni le set de la propriété Prix ne sont codés... le test passe!

Un bon test pour le changement du prix est donc :

testclass { test

Produit prod = new Produit("T#", 123);

assert prod.Prix != 0;

runtest prod.Prix = 0;

assert prod.Prix == 0;

Maintenant, ce cas est exclu. Si le constructeur n'initialise pas la propriété Prix, le premier assert échouerait et, comme il est avant l'instruction runtest, le test est 'Invalid' et non pas échoué! De plus, d'un point de vue logique d'affaires, on voit bien que mettre le prix à 0 le fait passer d'une valeur non nulle à nulle et donc qu'un prix nul est acceptable.

Quoi tester ? T# incite à dire ce qui est testé, non pas par des noms de classes et méthodes les plus appropriés possibles, mais en l'indiquant clairement. Ainsi, le test précédent devrait s'écrire :

testclass for Produit { test Prix set

Produit prod = new Produit("T#", 123);

assert prod.Prix != 0;

runtest prod.Prix = 0;

assert prod.Prix == 0;

Les avantages sont les suivants : Les éléments testés sont validés par le compilateur. On retrouve facilement les tests qui testent une déclaration précise Il est possible d'exécuter tous les tests d'une déclaration sans savoir où ils sont! Dans Visual Studio, placez votre curseur d'édition dans le code d'une déclaration et utiliser le menu contextuel pour exécuter 'Run T# Tests'! Particulièrement intéressant lorsque l'on retravaille une déclaration sans changer ses objectifs (refactoring) Il est possible d'extraire tous les tests d'une déclaration pour les présenter graphiquement dans le 'Tests Runner'.

Contextes

Comme pour tout système de test, il y a beaucoup de redondances dans l'écriture des tests. En effet, il est nécessaire d'avoir plusieurs tests pour chaque déclaration d'une classe et, généralement, une classe possède plusieurs déclarations. Dans tous ces tests, il sera nécessaire de créer une instance de la classe à tester.

Tous les systèmes de test proposent une méthode à appeler avant tout test et une à appeler après tout test. T#, lui, ne propose qu'une seule méthode.

Cela procure ainsi les avantages suivants : utilisation de variables locales et non pas systématiquement d'instances. utilisation d'instructions using, try...catch, try...finally etc pour encadrer tout test répétition du mot clé runtest pour rouler les tests plusieurs fois! directement ou dans une boucle. Particulièrement utile pour exécuter tous les tests dans différents contextes

Contexte de chaque test

La forme la plus simple de contexte est le context de chaque test. C'est celle utilisée par défaut.

Les tests sont exécutés, mais pas directement. Le contexte, introduit par une méthode déclarée par le mot clé testcontext, est appelée pour chaque test. Le mot clé runtest indique l'endroit où le test doit réellement s'exécuter.

Ainsi, dans notre exemple, nous voulons créer l'instance avec une seule ligne de code, mais il faut créer une instance pour chaque test :

testclass for Produit { Produit prod;

testcontext

prod = new Produit("T#", 123); runtest;

test Prix set // valeur minimale

assert prod.Prix != 0;

runtest prod.Prix = 0;

assert prod.Prix == 0;

test Prix set // valeur valide quelconque

assert prod.Prix != 12;

runtest prod.Prix = 12;

assert prod.Prix == 12;

Différents niveaux de contexte

En T#, le contexte se situe à trois niveaux : test : le code du contexte est effectué pour chaque test. Le test lui-même étant effectué à l'appel de runtest dans le contexte. Contexte par défaut. testclass : le code du contexte est effectué pour la classe de test. Les tests de la classe de test étant effectués à l'appel de runtest dans le contexte. testassembly : le code du contexte est effectué pour l'ensemble des classes de test de l'assemblage. Les tests étant effectué à l'appel de runtest dans le contexte.

Dans cet exemple, les tests seront exécutés deux fois, sans avoir à les écrire deux fois : pour une connexion vers une base de données SQL Server. pour une connexion vers une base de données Oracle.

testclass { IDbConnection connexion;

testcontext

testclass

connexion = new SQLConnection(...); runtest;

connexion = new OracleConnection(...); runtest;

... }

Quel cas tester ?

Lors de l'écriture de tests unitaires, le problème le plus classique est : « Quel cas dois-je tester ? ». En effet, une même déclaration doit être testée dans différents cas. Un des exemples précédents traitait du prix d'un produit représenté par une propriété. Combien faut-il de tests et quels sont ces tests dans un tel cas?

Dans les systèmes de tests classiques, c'est encore une fois le nom du test qui dit quel cas est testé (ou un commentaire, comme dans notre exemple précédent). Cela donne des noms souvent très longs et pas nécessairement clairs... ni mis à jour.

T# introduit un nouveau mot clé pour exprimer le cas testé : when suivi d'un cas à tester. Ainsi, l'exemple des tests du prix d'un produit devrait être :

testclass for Produit { Produit prod;

testcontext

prod = new Produit("T#", 123); runtest;

test Prix set when MinIncluded.IsMin

assert prod.Prix != 0;

runtest prod.Prix = 0;

assert prod.Prix == 0;

test Prix set when MinIncluded.IsAboveMin

assert prod.Prix != 12;

runtest prod.Prix = 12;

assert prod.Prix == 12;

Cas manquants

En fait, ce qui suit le mot clé when est un cas parmi plusieurs fonctionnant ensemble, décrits par un critère. Dans notre exemple, le critère est MinIncluded qui combine deux cas normaux (IsAboveMin et IsMin) et un cas d'erreur (BelowMinCase).

Il suffit donc d'identifier qu'un prix de produit a une valeur minimale (0) pour identifier qu'il faut tester selon le critère MinIncluded. Ce critère définissant trois cas, il va falloir écrire 3 tests pour tester cette propriété, un pour chaque cas défini dans le critère.

Pour l'instant, nous n'avons que deux cas définis (les cas normaux). Dès la compilation, T# indique les cas manquants : MinIncludedCriteria.BelowMinCase.

Expressions de cas

En réalité, après un when, une expression de cas est utilisée. Cette expression peut être un cas simple d'un critère ou une combinaison de critères.

Les opérateurs suivants existent : && (et logique) : combine toutes les possibilités entre deux critères, sachant que seules les cas normaux sont combinables, les cas d'erreur devant être testés séparément. || (ou logique) : regroupe deux cas dans un même test. ce n'est pas une bonne idée, mais cela peut être nécessaire d'exprimer la situation d'un même test exécutant plusieurs tests avec paramètres.