cogimator.net

Une ligne à la fois...

De 0 a un package Nuget

En préparant un projet de démo AngularJS + Bootrap + Web Api (article a venir !), j’ai eu besoin de générer des données de test. Après quelques recherches infructueuses, j’ai donc choisi de monter mon petit projet. Ca tombe bien, j’avais envie de coder un petit projet “utilitaire”, et de me faire plaisir avec une API de type “Fluent”.

Une première version est dores et déjà disponible sur github : https://github.com/mathieubrun/Cogimator.SampleDataGenerator. J’aurais l’occasion de vous en reparler dans un article à venir (ca commence a faire beaucoup d’articles à venir…).

Apres l’avoir référencé “manuellement” dans mon projet de démo AngularJS, je me suis demandé comment optimiser le déploiement de mon générateur de données. Ca tombe bien, j’avais envie de créer un package nuget.

 

Etape 1 : création du projet

Rien d’exceptionnel, une solution Visual Studio standard, avec l’option “Activer la restauration des packages nuget” activée, ceci afin d’avoir un nuget.exe accessible facilement en ligne de commande depuis le dossier de la solution.

Etape 2 : création d’un compte Nuget.org

Ici, toujours rien de complètement fou, on s’enregistre, et on note bien l’api Key, pour l’activer sur le poste de développement.

 

Etape 3 : création du fichier .nuspec

Ici, les choses peuvent un peu se compliquer, selon le temps que l’on souhaite consacrer à la documentation Nuget. Pour faire simple, il faut initialiser un fichier .nuspec avec la commande suivante

.nuget\nuget.exe spec

Ensuite, il faut déplacer ce fichier dans le dossier du projet qui va servir a générer le package, et lui donner le même nom que le .csproj :

image

Enfin, il ne reste plus qu’a modifier le contenu du fichier nuspec pour l’adapter a votre projet.

Etape 4 : publication manuelle

La création du package est maintenant possible, avec la commande

.nuget\nuget.exe pack Cogimator.SampleDataGenerator\Cogimator.SampleDataGenerator.csproj

Nuget s’appuiera sur les .csproj et .nuspec pour générer le package.

Il ne reste plus qu’a l’uploader sur nuget.org.

Etape 5 : le serveur d’intégration continue

J’ai choisi TeamCity pour remplir ce rôle, principalement en raison de sa simplicité, et aussi parce qu’il est développé par JetBrains (auteurs de ReSharper, donc pas vraiment des amateurs).

Je ne rentre pas dans les détails de l’installation, du type next, next, next. Comme j’exécute TeamCity sur mon poste de travail, J’ai juste changé le compte d’exécution des services pour “System account”. Sur un serveur dédié j’aurais crée un user spécifique (car c’est très mal de faire tourner des services avec System account !)

Une fois le serveur installé, il ne reste plus qu’a se rendre sur l’adresse http://localhost (avec le port spécifique si vous l’avez configuré ainsi) pour finaliser l’installation.

Ensuite, il faut créer un nouveau projet, avec un configuration associée. Ici, les valeurs par défaut feront très bien l’affaire.

Une fois le projet créer, il faut lui associer un contrôle de code source. TeamCity supporte git nativement, l’url présente sur la home de votre projet doit être reprise :

image

Petite subtilité, j’ai repris l’exécutable fourni avec le client github (dans ) et je l’ai copié dans un dossier spécifique :

image

Les étapes de build sont également aisées à configurer (je repense au temps passé a pondre du xml pour CruiseControl.NET…). La première étape est la compilation. Si vous avez bien paramétré le contrôle de code source, vous pouvez déjà choisir le fichier .sln avec le picker a droite du champ “solution file path” :

image

Nous arrivons maintenant aux étapes concernant nuget. L’ajout d’une tache “Nuget pack” vous aménera a l’écran suivant :

image

Ici, il faut bien penser à aller dans “Nuget settings” pour télécharger un client nuget pour TeamCity. Il est également possible de publier les packages nuget dans un feed local au serveur teamcity. Ce qui peut s’avérer plutôt pratique pour les développements en entreprise… Il est aussi à noter que le package sera généré avec un numéro de version correspondant par défaut au numéro de build. Ce qui s’avère pratique pour les mises à jour du package.

Enfin, la publication se fait avec la tache “Nuget publish” :

image

Une fois le build complété avec succès, on peut vérifier la présence du package sur nuget.org :

image

Integration continue : Partie 5, Tests unitaires

Tout d'abord, un test unitaire, quesako ? Un test unitaire est un bout de code qui va tester un bout de code de votre application. Unitairement. La partie "unitairement" signifie qu'on va tester une "unité" de notre code, par exemple, une classe et ses méthodes. Si on commence à tester plusieurs classes ensemble, on parlera plutôt de tests d'intégration. Même si dans la pratique, l'outil (NUnit) restera le même.

Prenons une méthode "simple" :

public class StringUtil 
{ 
    public static string GetLength( string input ) 
    { 
        return input.Length; 
    }
}

Son test unitaire ressemblera à ceci (on ajoutera une référence à nunit.framework.dll, situé dans le répertoire d'installation de NUnit):

[TestFixture] 
public class StringUtilTest 
{ 
    [Test] 
    public void GetLenghTest() 
    { 
        Assert.That( StringUtil.GetLength( "ab" ) == 2 ); 
        Assert.That( StringUtil.GetLength( "abc" ) == 3 ); 
        Assert.That( StringUtil.GetLength( "abcd" ) == 4 ); 
    } 
} 

Jusqu'ici tout va bien, on vient de vérifier que le fonctionnement interne de notre méthode est correct, et nous avons 100% de couverture de code. Toutefois, 100% de couverture de code ne signifient pas 0% de bugs. En effet, les tests unitaires ne peuvent pas tester du code inexistant, comme un contrôle d'erreurs par exemple... Nous parlerons dans ce cas de couverture d'états.

Justement, un utilisateur nous soumet un bug : "lorsque je passe null à la méthode, elle plante avec un NullReferenceException !". Ici, deux solutions, soit nous décidons de renvoyer 0 si on passe null à notre fonction GetLength, soit on remonte une exception de type "ArgumentNullException". Partons sur la 2ème solution. Nous pourrions immédiatement modifier le code de notre méthode, mais tout d'abord, modifions notre test unitaire :

[code:c#]

[TestFixture] 
public class StringUtilTest 
{ 
    [Test] 
    public void GetLenghtTest() 
    { 
        Assert.That( StringUtil.GetLength( "ab" ) == 2 ); 
        Assert.That( StringUtil.GetLength( "abc" ) == 3 ); 
        Assert.That( StringUtil.GetLength( "abcd" ) == 4 ); 
    } 

    [Test] 
    [ExpectedException(typeof(ArgumentNullException))] 
    public void GetLenghtArgNulTest 
    { 
        StringUtil.GetLength( null ); 
    } 
} 

Au lancement du jeu de test, GetLenghtArgNulTest devra logiquement échouer : on attend un ArgumentNullException, on reçoit un NullReferenceException. C'est évidemment voulu : nous utilisons les tests unitaires pour reproduire un "bug" avant de corriger celui-ci. Cela permet la non régression : une fois un bug soumis, un test est écrit, et si ce test échoue après la correction du bug, on a régressé. Nous n'augmentons pas encore notre couverture de code : le bug n'est pas corrigé. Par contre, nous améliorons notre couvertures d'états.

Corrigeons maintenant notre code :

public class StringUtil 
{ 
    public static string GetLength( string input ) 
    { 
        if( input == null ) throw new ArgumentNullException( "Merci de ne pas passer de null!"); 
        return input.Length; 
    } 
} 

Tout est maintenant au vert!

Pourquoi faire tout cela, écrire plein de lignes de code en plus, maintenir des tests unitaires ? Pour la fiabilité, et la maintenance future de l'application : une modification dans une partie de l'application non couverte pas les tests unitaires est une modification "sans filet". Il n'est pas possible, avant de tester manuellement, de savoir si la modification a introduit un bug. Si je modifie le fonctionnement interne de GetLength, j'aurai les tests unitaires qui m'indiqueront que, dans notre test, dans le cas des paramètres passés en entrée, les valeurs de retour sont bonnes.

On peut également utiliser NUnit pour "tester" une application avant même d'avoir une interface graphique. Le test d'une couche d'accès au données s'en trouve simplifié.

Enfin, un code qui est écrit à l'avance pour être testable est souvent modularisé et architecturé plus clairement : séparation des rôles, utilisation de couches et d'interfaces.

Même si ce billet peut paraître simple, voire simpliste dans son approche des tests unitaires, j'espère qu'il vous aura donné envie d'essayer.

Integration continue : Partie 4, Microsoft Source Analysis Tool for C#

Initialement, la partie 4 devait parler de tests unitaires, mais la sortie récente de cet outil mérite qu'on fasse un petit détour. Concrètement, MSAT va effectuer une analyse statique de votre code. Il va donc vous indiquer les parties de votre code qui ne se conforment pas à un standard de codage. Il est possible de le configurer selon ses besoins (par exemple, la règle qui dit que les tabulations c'est mal à l'air de faire couler beaucoup d'encre...). Donc attention, le but d'un tel outil n'est pas de définir une norme de codage pour tous les développeurs C# du monde (rien que ça), mais plutôt de maintenir une cohérence au sein des équipes travaillant sur un même projet.

Installation de l'outil

Microsoft Source Analysis Tool for C# peut être téléchargé ici : http://code.msdn.microsoft.com/sourceanalysis/Release/ProjectReleases.aspx?ReleaseId=1047. L'installation est très simple, il faut juste s'assurer que la partie MSBuild est bien installée, sinon, pas d'intégration dans notre processus de build.

Intégration dans vos projets

Une fois l'outil installé, une nouvelle entrée apparait dans le menu "Outils" de Visual Studio : "Run Source Analysis". Elle vous permet d'exécuter l'analyse du code directement dans Visual Studio, et affichera les résultats dans l'onglet "Source Analysis". Un double clc sur un message vous emmene sur la ligne concerné. Rien d'inhabituel donc.

Il est également possible d'intégrer l'analyse de votre code source directement dans le procéssus de build, et là, celà devient vraiment intéressant : lorsqu'un développeur lancera une compilation, toutes les erreurs liées à l'analyse du code source apparaitront en tant que warning dans l'onglet "Liste d'erreurs".

Pour permettre cela, deux solutions :

Si MSAT est installé sur tous les postes de développeur

Ouvrez chaque fichier .csproj pour lequel vous souhaitez analyser le code, et modifiez le de la façon suivante :

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 
    [...] 
    <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> 
    <Import Project="$(ProgramFiles)\MSBuild\Microsoft\SourceAnalysis\v4.2\Microsoft.SourceAnalysis.targets" /> 
    [...] 
</Project> 

Si MSAT n'est pas installé sur tous les postes de développeur

Pour faire suite au billet sur l'arborescence projet, vous pouvez maintenant rajouter un autre répertoire à la racine de la solution : "External" (par exemple... vous pouvez aussi l'appeler Tools, ou Trucs, peu importe!). Créez un répertoire "SourceAnalysis", et copiez y le contenu du répertoire "C:\Program Files\Microsoft Source Analysis Tool for C#". Maintenant, ouvrez chaque fichier .csproj pour lequel vous souhaitez analyser le code, et modifiez le de la façon suivante :

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 
    [...] 
    <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> 
    <Import Project="..\..\External\SourceAnalysis\Microsoft.SourceAnalysis.targets" /> 
    [...] 
</Project> 

Ensuite, intégrez ce répertoire dans le contrôle de code source. Les développeurs bénéficieront de l'analyse de code source dès qu'ils mettront leur copie locale à jour. Lors du premier rechargement du projet, vous obtiendrez un message d'avertissement :

avertissement_securite  

Choisissez de charger normalement le projet.

Et voilà, l'analyse statique intégrée dans Visual Studio, et dans le processus de compilation (et dans CruiseControl, de fait!).

Integration continue : Partie 3, Arborescence Projet

Arborescence projetAvant d'aller plus loin, je vais rapidement décrire l'arborescence projet que je vais utiliser pour les billets à venir (cf image):

  • Apps : applications Winforms, Console, WebForms, tout ce qui est exécutable
  • Libs : toutes les bibliothèques de classes, ou tout ce qui n'est pas exécutable
  • References : toutes les librairies externes référencéés dans votre solution (NHibernate, Castle Project, etc)(non présent dans la solution visual studio, uniquement dans l'arborescence "physique")
  • Scripts : scripts de compilation, de création de base de données
  • Tests : tests unitaires

Important : si vous créez des dossiers de solution dans Visual Studio, attention lors de la création d'un nouveau projet : par défaut, celui-ci est placé à la racine de votre dossier de solution. Il en est de même pour les éléments de solution (par ex : Demo.config et Demo.nunit), il est préférable de les créer physiquement dans le dossier voulu, et de les ajouter à la solution dans un deuxième temps (via "Ajouter un élément existant").

Cette arborescence n'est évidemment pas figée dans ses termes, il s'agit juste de "classer" un peu notre solution. Il est également possible de créer des sous dossiers, comme par exemple "Console" ou "WebApps" dans "Apps".

Concernant les librairies externes, j'ai pris le parti de les lier à chaque solution : cela permet d'éviter les problèmes lors d'une mise à niveau de librairie. Si tous vos projets référencent la meme librairie (NHibernate par exemple), qu'une mise à jour avec un changement d'API de leur part est effectuée, tous vos projets sont impactés. En liant les librairies par projet, vous pouvez effectuer les mises à jour dans chaque projet, si le besoin s'en fait ressentir.

Integration continue : Partie 2, Controle de code source

Pour cette partie sur le contrôle de code source, nous allons mettre en place et utiliser Subversion. Il vous faudra télécharger VisualSVN Server (http://www.visualsvn.com/server/), ainsi que TortoiseSVN (http://tortoisesvn.tigris.org/). Vous pouvez également installer AnkhSVN (http://ankhsvn.open.collab.net/) si vous souhaitez bénéficier de l'intégration dans Visual Studio (attention, pour Visual Studio 2008, prenez bien la version 1.0.3 ou supérieure).

Une fois VisualSVN Server installé sur le serveur, TortoiseSVN et AnkhSVN installés sur les clients, nous allons pouvoir commencer.

Sur le serveur

En premier lieu, il va falloir créer un repository. Un repository est un espace où sera stocké le code source. Toute modification de fichier (commit) dans le repository entraînera une incrémentation du numéro de version (revision) global au repository. Le repository sera physiquement stocké à l'endroit spécifié lors de l'installation de VisualSVN Server. C'est ce répertoire que vous devrez sauvegarder régulièrement. Pour aller plus loin : Subversion in action : revisions.

Donnez un nom explicite à votre repository (le nom de la solution Visual Studio, le codename du projet...). Attention à bien laisser cocher la case "Create default structure". Cette structure de répertoire est une convention :

  • trunk : code source "actuel" du projet
  • tags : versions spécifiques du projet, comme par exemple les différentes livraisons ou versions installées en production
  • branches : version en développement : ajout de nouvelles fonctionnalités.

Tous les détails sont expliqués dans ce post de Bill Simser.

Le repository étant crée, nous allons en autoriser l'accès à nos développeurs. Créez un (ou plusieurs) utilisateurs, via "Create new user" de la page d'accueil.

Il ne reste plus qu'à créer un groupe "developpers", via "Create new group" de la page d'accueil et enfin d'associer notre utilisateur au groupe.

La dernière étape consiste à accorder les droits d'accès à notre groupe sur le repository (ou sur tous les repositories, selon votre goût). Pour cela, clic-droit sur un objet dans l'arborescence, et "Security".

Avant de passer sur le poste de développeur, pensez à note l'url du repository (normalement de la forme http://serveur:8080/svn/NomDuRepository).

Sur le poste de développeur

Nous allons créer une copie locale du repository. Toute modification dans cette copie locale est répercutée via un "Commit", et toute modification du repository est repercutée sur la copie locale via un "Update". Pour gérer cette copie locale, nous allons utiliser TortoiseSVN (AnkhSVN reprend les mêmes principes, mais directement dans Visual Studio).

Après avoir installé TortoiseSVN, créez un nouveau dossier (par ex: d:\IntegrationContinue), faites un clic droit sur ce nouveau dossier, et choisissez "SVN Checkout". Renseignez l'url du repository crée plus tot (http://serveur:8080/svn/IntegrationContinue/trunk), et validez. Attention, l'url doit être suivie de "trunk". Votre copie locale est maintenant créée !

Ensuite, toujours dans ce dossier : faites un clic droit -> TortoiseSVN -> Settings. Dans l'entrée "General", "Subversion", "Global ignore patterns", vous pouvez saisir "bin obj *.suo *.user". Ceci indiquera a TortoiseSVN de toujours ignorer ces répertoires et ces fichiers, et évitera de placer les exécutables ainsi que les fichiers temporaires de compilation sous controle de code source. Les paramètres de solution/projet par utilisateur ne seront également pas placés sous contrôle de code source.

ignore_pattern

Il est également possible de définir quels fichiers/répertoires seront ignorés, et ce par projet, dans la fenêtre de commit :

ignore_property

Enfin, tant que vous êtes dans les paramètres de TortoiseSVN, vous pouvez également ajouter les valeurs suivantes dans la partie "Icon overlays" :

exclude

Cela vous évitera des petits soucis de fichiers vérouillés aléatoirement.

Ce qui nous amène à une autre convention : tout ce qui n'est pas nécessaire à la compilation n'est pas nécessaire dans le contrôle de code source. En effet, à quoi bon stocker N versions d'un code source, ainsi que N versions de l'éxecutable produit par ce code source ?

Processus de mise à jour des sources

Pour le moment, nous avons :

  • un serveur SVN
  • un client SVN

Il ne nous reste plus qu'à créer une nouvelle solution Visual Studio : si vous avez crée le dossier "IntegrationContinue" dans d:\, créez la solution également dans d:\, de manière à ce que le fichier IntegrationContinue.sln se trouve dans d:\IntegrationContinue\IntegrationContinue.sln. Ajoutez vos projets à cette solution.

Une fois ceci fait, retournez dans le dossier d:\IntegrationContinue, pour faire un "commit" avec un clic droit -> TortoiseSVN -> Commit, sélectionnez tous les fichiers, et cliquez sur "Commit". Tous vos fichiers se trouvent maintenant sous contrôle de code source!

Ce qui nous amène à une première version du processus de mise à jour du code source sous SVN.

1. Bob (notre développeur) fait un update de sa copie locale pour obtenir la dernière version du code source
2. Bob fait une modification dans le code
     2.1 Si le code ne compile pas, retour en 2.
     2.2 Si le code compile, passer en 3.
3. Bob fait un update de sa copie locale pour obtenir les modifications qui auraient été effectuées depuis 1.
     3.1 Si le code ne compile pas, retour en 2.
     3.2 Si le code compile, passer en 4.
4. Bob fait un commit pour mettre à jour le code source sur SVN.

Le point 3 est important, il permet de s'assurer que les modifications effectuées en 2 ne sont pas incompatibles avec une éventuelle modification ayant eu lieu entre 1 et 3.

Enfin, le point le plus important : le code source situé dans le contrôleur de code source (plus précisément dans la partie trunk), doit OBLIGATOIREMENT compiler. Si le code ne compile pas, on ne fait pas de commit de ce code.

Integration continue : Partie 1, Introduction

Je vais tacher de vous expliquer comment mettre en place un environnement d'intégration continue au travers de cette petite série de billets.

Tout d'abord, il faut un environnement de développement : Visual Studio 2005 (ou 2008).

Les articles suivants viendront, dans l'ordre :