Integración continua en .Net y basta de cháchara: Parte 1

En esta serie de posts voy a explicar como empezar a trabajar con integración continua. Como el título lo dice, la razón por la que una persona o un equipo de desarrollo debería usar integración continua, trasciende el alcance de estos artículos. Deberías usarlo y ya!
El primer post va a estar dedicado a explicar como crear un “build script” para una solución de .Net.

Nota: Quiero agradecerle a Germán Schuager por enseñarme varias de las cosas que voy a explicar.

El script realizará los siguientes 4 pasos:

  1. Actualizar el número de versión en los archivos AssemblyInfo.cs, esto hará que nuestros resultados; (dlls, exes, etc) tengan un número de versión cuyo Build (recordar esquema Major.Minor.Build) coincida con la revisión de subversion.
  2. Compilar la solución, esto incluye todos sus proyectos.
  3. Ejecutar los tests de todos los proyectos de tests.
  4. Y finalmente copiar a una carpeta especifica el resultado de nuestro build.

Para ello utilizaremos la herramienta MsBuild.

Estructura de directorios

A continuación se muestra una estructura de directorios típica que se suele utilizar:

image

 

  1. Es un ejemplo de estructura de directorios que suelo utilizar.
  2. El contenido de la carpeta “Tools”. Se puede ver adentro dos carpetas
    • “msbuildtasks”: En esta carpeta colocaremos los binarios del proyecto MsBuild Community Tools. Este proyecto tiene Tasks adicionales para MsBuild.  Ya que el mismo, no conoce como interactuar con subversion, nunit u otras herramientas out-of-the-box. Un ejemplo de los archivos que van en este directorio se puede ver en ( 3 )
    • “nunit”: En esta carpeta colocaremos los binarios de nunit. Un ejemplo de los archivos que van en este directorio se puede ver en ( 4 ).

Common.targets

Al igual que todo el contenido de la carpeta “Tools”, se puede copiar prácticamente sin modificación alguna, de proyecto en proyecto. En el archivo common.targets resolveremos algunos asuntos, que ha continuación se pueden ver como comentarios de xml:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">

    <!-- Definicion  de Paths y algunas variables en general. -->
    <PropertyGroup>
        <MSBuildCommunityTasksPath>.</MSBuildCommunityTasksPath>
        <ToolsPath>Tools</ToolsPath>
        <NUnitPath>$(ToolsPath)nunit</NUnitPath>
        <OutputPath>output</OutputPath>
        <ReportsPath>$(OutputPath)Reports</ReportsPath>
        <ResultPath>$(OutputPath)Build</ResultPath>
    </PropertyGroup>
    
    <!-- Importamos MsBuild Community Tasks. -->
    <Import Project="msbuildtasksMSBuild.Community.Tasks.Targets" />
    
    <PropertyGroup>
        <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
        <FullVersion>$(BUILD_NUMBER)</FullVersion>
    </PropertyGroup>

    <!-- Tarea para actualizar mis AssemblyInfo.cs -->
    <Target Name="UpdateAssemblyInfos">
        <Message Text="Updating version numbers to $(FullVersion)..." />

        <CreateItem Include=".**AssemblyInfo.cs">
            <Output TaskParameter="Include" ItemName="AssemblyInfos"/>
        </CreateItem>
        
        <FileUpdate Condition="'$(FullVersion)' != ''"
            Files="@(AssemblyInfos)"
            Regex="[s*assemblys*:s*AssemblyVersions*(s*&quot;[d.*]+&quot;s*)s*]"
            ReplacementText="[assembly: AssemblyVersion(&quot;$(FullVersion)&quot;)]" />
        <FileUpdate Condition="'$(FullVersion)' != ''"
            Files="@(AssemblyInfos)"
            Regex="[s*assemblys*:s*AssemblyFileVersions*(s*&quot;[d.*]+&quot;s*)s*]"
            ReplacementText="[assembly: AssemblyFileVersion(&quot;$(FullVersion)&quot;)]" />
        <FileUpdate Condition="'$(FullVersion)' != ''"
            Files="@(AssemblyInfos)"
            Regex="[s*assemblys*:s*AssemblyInformationalVersions*(s*&quot;.*&quot;s*)s*]"
            ReplacementText="[assembly: AssemblyInformationalVersion(&quot;$(FullVersion)&quot;)]" />
    </Target>
    
    <!-- Tarea para compilar mi solucion en la configuracion seleccionada (debug o release)  -->
    <Target Name="DefaultBuild">
        <Message Text="Building $(SolutionFile)..." />
        <MSBuild Projects="$(SolutionFile)"
           Properties="Configuration=$(Configuration)" 
           ContinueOnError="false"   />
    </Target>
    
    <!-- Tarea para correr todos mis tests en el grupo de items definido para tal caso TestAssemblies -->
    <!-- Esta tarea genera reportes de nunit en format xml. En el directorio $ReportsPath. -->
    <Target Name="RunTests">
        <Message Text="Cleaning test reports folder..." />
        <RemoveDir Directories="$(ReportsPath)" />
        <MakeDir Directories="$(ReportsPath)" />
        <Exec Command="$(NUnitPath)nunit-console-x86.exe @(TestAssemblies) /xml=$(ReportsPath)%(TestAssemblies.Filename).xml" 
            IgnoreExitCode="true" />
    </Target>
    
    <!-- Tarea para copiar el resultado de mi proyecto al directorio $ResultPath -->
    <Target Name="CopyBuildResult">
        <Message Text="Cleaning build output folder..." />
        <RemoveDir Directories="$(ResultPath)" />
        <MakeDir Directories="$(ResultPath)" />
        <Message Text="xxx @(BuildResult)" />
        <Copy SourceFiles="@(BuildResult)" DestinationFolder="$(ResultPath)" />
    </Target>
    
</Project>

Default.build

Este archivo sí es propio de cada solución, y un ejemplo para este proyecto sería el siguiente:

<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">

    <!--Importar common.targets-->
    <Import Project="Toolscommon.targets" />

    <!--declaro el path relativo al archivo de solución a compilar-->
    <PropertyGroup>
        <SolutionFile>MiProyecto.sln</SolutionFile>
    </PropertyGroup>
    
    <!--declaro los proyectos de tests que se deberían incluir-->
    <ItemGroup>
        <TestAssemblies Include="*Testsbin$(Configuration)*Tests.dll" />
    </ItemGroup>
    
    <!--declaro la ubicación de los proyectos a incluir-->
    <ItemGroup>
        <BuildResult Include="MiProyecto.GUIbin$(Configuration)*.*" />
    </ItemGroup>        
    
    <!--declaro el target All-->    
    <Target Name="All" DependsOnTargets="UpdateAssemblyInfos; DefaultBuild; RunTests; CopyBuildResult">
    </Target>
    
</Project>

$(Configuration) es una variable que dice si estamos en Debug o Release.
La especificación de proyectos de Tests  puede parecer un poco rara; podríamos haberla declarado de la siguiente manera

<ItemGroup>
    <TestAssemblies Include="MiProyecto.Domain.Testsbin$(Configuration)MiProyecto.Domain.Tests.dll" />
    <TestAssemblies Include="MiProyecto.Data.Testsbin$(Configuration)MiProyecto.Data.Tests.dll" />
</ItemGroup>

El problema que yo tuve con esto, era que cada vez que agregaba un proyecto de tests (cosa que suelo hacer a menudo) tenía que acordarme de modificar este script. Lo cual es poco práctico, por lo tanto aprovechando mi convención [Proyecto a Testear].Tests utilizaremos los wildcards (comodines) de MsBuild, de esta forma podremos agregar proyectos de tests a la solución sin necesidad de modiricar este archivo. Una cosa que deberemos tener especial cuidado, es cuando desde un proyecto de tests referenciamos otro proyecto de tests, cosa que en general trato de evitar.

Build.bat

El archivo build.bat lo utilizaremos para correr el script localmente, ya sea para probar el script, hacer deployment, etc. Este archivo tiene lo siguiente:

@echo off
call "%VS90COMNTOOLS%....VCvcvarsall.bat"
msbuild %~dp0default.build /t:All /nologo

La primer línea registra las variables de MsBuild así como el path donde se encuentra la herramienta, y la segunda ejecuta MsBuild. Si se usa Visual Studio 2008, se coloca %VS90COMNTOOLS%… mientras que si se usa 2010, se coloca %VS100COMNTOOLS%. Esto presupone que la maquina donde ejecutaremos el batch tiene instalado Visual Studio. De más esta decir que este no será el caso de nuestro servidor de integración continua.

Cabe destacar que MsBuild, no solamente viene con Visual Studio, sino que también viene con .Net Framework SDK.

Hay más…

Scripts: Lo que hice aquí con MsBuild, puede hacerse con muuuchas otras herramientas diferentes; entre ellas nant, rake, psake, etc. Cada una de ellas tiene sus ventajas y desventajas. Actualmente elijo MsBuild por que hace exactamente lo que quiero, dispone de muchos ejemplos, es sencillo crear nuestras propias tasks directamente en C#, y la mejor de todas… no hace falta instalar prácticamente nada.

Control de versiones: Si están usando un DCVS como Mercurial o GIT se hace un poco mas complicado el tema de asignar un número de versión a los ensamblados ya que estos sistemas no utilizan números para en sus checkins, pero he visto que hay tareas para tal propósito como por ejemplo MSBuild Mercurial Tasks.

Frameworks de Testing: Todos los frameworks de tests unitarios disponen de una herramienta para correr los tests en modo consola, así que debería ser trivial en caso que usen algo como xUnit o MbUnit. En el caso particular de MsTests es mas sencillo ya que esta soportado nativamente por msbuild.

En el próximo post voy a mostrar como dar de alta esto en nuestro servidor de integración continua.


blog comments powered by Disqus
  • Categories

  • Archives