Configuration d'un serveur d'intégration continue - [Partie 2] - Tests unitaires
By Michael DELVA on Friday 15 January 2010, 16:03 - Miscellaneous - Permalink
TweetAprès avoir vu dans la première partie comment installer Hudson, configurer notre premier job et notre premier fichier NAnt, nous allons maintenant voir comment lancer automatiquement dans le processus de compilation l'exécution de tous les tests unitaires de la solution, bien entendus créés avec le framework de test xUnit.
Préambule
Lorsqu'on regarde les différents tutoriaux qui expliquent comment lancer les tests unitaires depuis NAnt, on s'aperçoit que dans le cas des frameworks de tests les plus courants (nUnit par exemple), les auteurs utilisent la console fournie avec le framework de test, en ligne de commande, en lui passant en paramètre la liste des assemblies où se trouvent les tests. La console va alors exécuter les tests et exporter un rapport contenant le compte-rendu de ces tests. Le souci avec xUnit, c'est que la console n'accepte pas d'assemblies multiples. On est donc obligé de la lancer pour chacune des assemblies à tester, et de créer autant de fichiers de rapports qu'on a d'assemblies à tester. Ça n'est pas un gros souci puisque Hudson peut utiliser de multiples fichiers XML pour générer son rapport sur les tests unitaires. Mais le souci est que d'une part ça oblige à complexifier le fichier NAnt, et d'autre part, ça rend la génération des rapports sur la couverture de code plus compliquée à mettre en place. Je vous épargnerai donc toutes les recherches et les soucis que j'ai eu à essayer d'utiliser directement la console de xUnit, et à la place vous proposer une solution plus simple, et surtout qui fonctionne :D
Gallio
La solution que je vous propose est de lancer tous les tests unitaires grâce à un logiciel qui s'appelle Gallio, décrit sur leur site comme:
The Gallio Automation Platform is an open, extensible, and neutral system for .NET that provides a common object model, runtime services and tools (such as test runners) that may be leveraged by any number of test frameworks.
Et dans cette suite de logiciels que propose Gallio, nous allons utiliser l'exécutable Gallio.echo, qui est une console qui va nous permettre d'exécuter nos tests unitaires. Mais avant d'ajouter une nouvelle tâche à notre fichier NAnt, nous allons d'abord voir comment gérer nos assemblies, pour nous faciliter le travail.
Organisation des assemblies
Pour le moment, lorsque nous compilons notre solution, les assemblies créées se trouvent dans le dossier Bin\Debug de chaque projet. Donc pour chaque projet de tests, si on veut le chemin depuis le dossier de la solution, nous avons: Solution\src\tests\ProjetXXX\Bin\Debug\. Si on veut tester toutes les assemblies, ça n'est pas évident car il faut dans ce cas référencer chaque projet. Pour nous faciliter le travail, nous allons créer tout un jeu de dossiers temporaires, qui seront recréés à chaque build par Hudson, dans lesquels nous copierons tous les fichiers que nous analyserons, et également tous les fichiers de sortie des outils d'analyse que nous allons utiliser (FxCop, nCover, etc...)
Pour ma part, je vais utiliser l'architecture suivante (le dossier BuildOutput étant à la racine de ma solution, au même niveau que le fichier .sln)
BuildOutput
| -> Assemblies
| -> app
| -> tests
| -> tools
| -> FxCopOutput
| -> NCoverOutput
| -> TestsOutput
Les noms des dossiers sont suffisamment clairs comme ça, donc je ne m'étendrai pas sur le sujet. Hormis pour le répertoire Assemblies. Se trouveront dans le dossier app toutes les assemblies des programmes, ainsi que les dlls dont ces programmes auront besoin pour fonctionner. Dans le sous répertoire tests se trouveront uniquement les assemblies des tests unitaires et les assemblies qu'elles testent, c'est à dire sans xUnit / Moq, etc... Ces dernières se trouveront dans le sous-dossier tools. Ce choix est dicté par ncover, qui par défaut va faire son rapport sur toutes les assemblies du dossier. Donc pour éviter que celui-ci prenne en compte les assemblies dont on n'est pas responsable, c'est plus pratique de les mettre dans un sous-dossier à part.
Maintenant que les explications sont faites, nous allons rentrer dans le vif du sujet et améliorer notre fichier NAnt. Et nous allons commencer par implémenter la création de ces répertoires d'output, ainsi que leur suppression préalable, pour toujours avoir la dernière version des rapports.
Première tâche à effectuer: supprimer le répertoire BuildOutput s'il existe, et ensuite le créer, avec tous les sous-dossiers nécessaires. On commence par ajouter les propriétés nécessaires:
<property name="build.output.dir" value="${solution.dir}/BuildOutput" />
<property name="tests.output.dir" value="${build.output.dir}/TestsOutput" />
<property name="fxcop.output.dir" value="${build.output.dir}/FXCopOutput" />
<property name="ncover.output.dir" value="${build.output.dir}/NCoverOutput" />
<property name="assemblies.output.dir" value="${build.output.dir}/${nant.settings.currentframework}.${platform::get-name()}-${project::get-name()}-${project.config}" />
<property name="app.assemblies.output.dir" value="${assemblies.output.dir}/app" />
<property name="tests.assemblies.output.dir" value="${assemblies.output.dir}/tests" />
<property name="tools.tests.assemblies.output.dir" value="${tests.assemblies.output.dir}/tools" />
Vous remarquerez que la propriété assemblies.output.dir est un peu plus complexe. Elle permet d'enregistrer les assemblies dans des dossiers différents selon qu'elles soient compilées en debug ou en relase, ou avec le framework 3.5 ou 4.0.
On ajoute ensuite une nouvelle tâche, qui va commencer par supprimer BuildOutput, avant de créer les sous-dossiers:
<target name="setup.output.directories">
<delete dir="${build.output.dir}" failonerror="false" />
<mkdir dir="${build.output.dir}" />
<mkdir dir="${tests.output.dir}" />
<mkdir dir="${fxcop.output.dir}" />
<mkdir dir="${ncover.output.dir}" />
<mkdir dir="${assemblies.output.dir}" />
<mkdir dir="${app.assemblies.output.dir}" />
<mkdir dir="${tests.assemblies.output.dir}" />
<mkdir dir="${tools.tests.assemblies.output.dir}" />
<echo message="Directories created" />
</target>
Maintenant que l'arborescence est créée, nous allons maintenant copier les assemblies des programmes et des tests vers leurs répertoires de destination:
<target name="move.assemblies" depends="setup.output.directories, compile.sources">
<echo message="Moving assemblies to ${assemblies.output.dir}" />
<copy todir="${tests.assemblies.output.dir}" flatten="true">
<fileset basedir="${solution.dir}">
<include name="**/tests/**/bin/${project.config}/Emidee.*.dll" />
</fileset>
</copy>
<copy todir="${tools.tests.assemblies.output.dir}" flatten="true">
<fileset basedir="${solution.dir}">
<exclude name="**/tests/**/bin/${project.config}/Emidee.*.dll" />
<include name="**/tests/**/bin/${project.config}/*.dll" />
</fileset>
</copy>
<copy todir="${app.assemblies.output.dir}" flatten="true">
<fileset basedir="${solution.dir}">
<exclude name="**/app/**/bin/${project.config}/*.vshost.exe" />
<include name="**/app/**/bin/${project.config}/*.dll" />
<include name="**/app/**/bin/${project.config}/*.exe" />
</fileset>
</copy>
</target>
Cette tâche a 2 dépendances logiques pour son bon fonctionnement: la création des répertoires, et la compilation des assemblies. On commence par copier les assemblies de tests vers le répertoire BuildOutput/net-4.0.win32-Foo-debug/tests depuis tous les dossiers bin/Debug ou bin/Release de chacun des projets de tests. Ensuite on va copier les dll contenues dans ces même répertoires, mais qui sont les assemblies satelites requises par nos tests (xUnit.dll, Moq.dll & co) dans le sous-répertoire tools. Et on finit par copier les dll et les exe des applications dans le dossier app.
Pour la suite des opérations, je pense utile de vous informer de la convention de nommage de mes assemblies: elles sont toutes préfixées par le nom de la solution (ici Emidee), puis ensuite le nom du projet (Commons par exemple). Puis s'il s'agit d'une assembly de tests, on va retrouver Tests. Et enfin l'extension. Ce qui donne par exemple Emidee.Commons.Tests.dll pour l'assembly qui contient les tests portant sur Emidee.Commons.dll.
Ceci étant dit, voyons comment exécuter nos tests avec Gallio.
Exécuter les tests unitaires
Toutes nos assemblies de tests étant dans le même dossier, nous n'avons qu'à configurer une nouvelle tâche dans NAnt, qui va appeler en ligne de commande Gallio.Echo et lui passer les bons paramètres. Voici le résultat final:
<property name="gallio.bin.dir" value="C:\Program Files\Gallio\bin" />
<property name="gallio.echo.path" value="${gallio.bin.dir}\gallio.echo.exe" />
<property name="gallio.echo.report.name" value="tests-reports" />
<property name="tests.assemblies" value="${tests.assemblies.output.dir}/Emidee.*.Tests.dll" />
<target name="run.tests" depends="compile.sources, move.assemblies" failonerror="false">
<echo message="Starting unit tests" />
<exec program="${gallio.echo.path}">
<arg value="${tests.assemblies}" />
<arg line="/rt:xml /v:quiet /ne" />
<arg value="/rd:${tests.output.dir}" />
<arg value="/rnf:${gallio.echo.report.name}" />
<arg value="/hd:${tools.tests.assemblies.output.dir}" />
</exec>
</target>
Les arguments correspondent à (dans l'ordre d'apparition dans le XML):
- Les assemblies à tester (Emidee.*.Tests.dll dans le dossier BuildOutput/net-4.0.win32-Foo-debug/tests)
- Rapport au format XML, Verbosity à Quiet, et no-echo-results (uniquement le rapport de fin, pour ne pas surcharger le log)
- Dossier de sortie du rapport (BuildOutput/TestsOutput)
- Nom du rapport (tests-reports.xml)
- Dossier où trouver les assemblies référencées par les tests (dans le sous-dossier tools)
Bien entendu, pour fonctionner, cette tâche a des pré-requis: que les projets aient été compilé, et que les assemblies aient été copiées.
Pour que ces tests soient lancés, il nous faut les ajouter à la tâche full.build:
<target name="full.build" depends="run.tests" />
Consulter les rapports des tests unitaires dans Hudson
C'est bien beau, nos tests vont être lancés par NAnt, et le fichier de rapport va être créé dans BuildOutput/TestsOutput. Ne serait-il pas super cool de pouvoir consulter ce rapport lorsque Hudson aura achevé le build? J'imagine que comme moi, vous pensez que si. Voici donc comment faire.
Le fichier XML écrit par Gallio, et contenant le rapport des tests a un format qui n'est pas reconnu nativement par Hudson. Mais heureusement, il existe un plugin Hudson qui va nous permettre de parser le fichier XML et d'afficher le rapport des tests unitaires directement depuis la page du projet dans Hudson. Après l'avoir installé, allez dans les propriétés du projet, localisez la ligne Publish Gallio test result report, tout en bas, et modifiez le chemin vers le XML comme suit:
Lancez votre build dans Hudson, et vous devriez avoir dans le bas de l'output de la console quelque chose comme:
BUILD SUCCEEDED
Total time: 33.6 seconds.
Recording Gallio tests results Finished: SUCCESS
Hudson a donc correctement utilisé le rapport de tests. Pour en voir le contenu, sur la page d'accueil de votre projet, cliquez sur ce lien:
Et vous arrivez sur le résumé du déroulement des tests:
En prime, au fur et à mesure de vos builds, Hudson vous gratifiera d'un graphique vous présentant l'évolution de la réussite de vos tests dans le temps.
Conclusion
Nous voilà désormais capables de compiler les sources, isoler les assemblies, lancer tous les tests unitaires de notre solution, et afficher le rapport de tests. En suivant la même logique de fonctionnement, nous verrons dans la troisième partie comment automatiser une analyse de code avec FxCop, puis dans la dernière partie de ce tutorial comment lancer une analyse de couverture de code par nos tests unitaires.