Automatizando la compilación con MSBuild y xUnit

En el proceso de integración continua hay que distinguir la compilación es sí, del servidor de integración continua que se encarga cada cierto tiempo o con cada checkin o push según entorno, de ejecutar dicha compilación.

Pasos de compilación en integración continua

El proceso de compilación consta de unos pasos que podemos realizar manualmente desde el IDE, según cada escenario puede variar pero a nivel general estos son:

  • Clean, paso donde se eliminan los binarios compilados anteriormente.
  • Build, paso donde compilan incrementalmente los binarios con los cambios del código fuente.
  • Rebuild, paso donde se combinan clean y build.
  • Restaurar paquetes de dependencias, paso donde se descargan los paquetes de NuGet si es necesario, este paso en Visual Studio 2015 viene integrado en el paso build.
  • Testing, ejecución de los test para validar el código fuente y verificar que se comporta como esta definido.

Abstrae procesos

Es importante que tengamos claros los pasos de la compilación y que pueden no depender del servidor de integración continua, porque mi recomendación es no depender del servidor de integración continua.

La mejor opción es tener un script de MSBuild con los pasos de la compilación y que se pueda ejecutar manualmente.

Al no depender del servidor podremos cambiar un servidor por otro de una forma más ágil. Al final esto es como programar, es decir, abstraemos para no acoplar procesos y hacer nuestro proceso global más escalable.

Script de MSBuild

Es un script que va a automatizar los pasos que podemos realizar manualmente.

Targets

Un script de MSBuild es un archivo XML donde los pasos de la compilación son Targets. Un Target es un proceso que se puede invocar de forma independiente y puede depender de otros targets.

Invocando un target individualmente

Desde linea de comandos podemos invocar un target del script individualmente. Vamos a ver un esquema de un script con targets vacíos de momento y como se ejecuta un target concreto.

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

  <Target Name="RestorePackages">
  </Target>

  <Target Name="Rebuild">
  </Target>

  <Target Name="Tests">
  </Target>

</Project>  
msbuild.exe CIBuild.msbuild /t:Test ← Esto ejecuta el target Tests

msbuild.exe CIBuild.msbuild /t:RestorePackages ← Esto ejecuta el target RestorePackages  

Invocando todos los targets

Para invocar todos los targets, podemos crear uno Default por ejemplo y le decimos que depende de todos los demás. En el orden que pongamos las dependencias será el orden en que se ejecutarán. Además al principio del script hay que indicar que este target es el de por defecto.

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

  <Target Name="Default" DependsOnTargets="RestorePackages;Rebuild;Test">
  </Target>

  <Target Name="RestorePackages">
  </Target>

  <Target Name="Rebuild" >
  </Target>

  <Target Name="Test">
  </Target>

</Project>  

Ahora al ejecutar el script sin indicar el target, se ejecutan todos y en el orden establecido.

msbuild.exe CIBuild.msbuild ← Esto ejecuta todos los targets  

Task

Son acciones que se realizan dentro de cada Target. MSBuild trae Tasks por defecto y también podemos definir nuestras propias task.

Creando un script

Descargar paquetes NuGet

Lo normal en los proyectos .Net es tener dependencias de terceros y gestionadas con NuGet. Si alguien se baja el código por primera vez y realiza la compilación con MSBuild no va disponer de las dependencias con lo que la compilación va a fallar.
Necesitamos descargar el ejecutable NuGet.exe y ponerlo en una carpeta tools por ejemplo dentro del directorio de nuestra solución.
Ahora ya podemos implementar el paso de descarga de paquetes utilizando la tarea Exec, que sirve para ejecutar comandos desde el terminal.

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

  <Target Name="Default" DependsOnTargets="RestorePackages;Rebuild;Test">
  </Target>

  <Target Name="RestorePackages">
    <Exec Command="&quot;tools\NuGet.exe&quot; restore &quot;TuSolucion.sln&quot;" />
  </Target>

  <Target Name="Rebuild" >
  </Target>

  <Target Name="Test">
  </Target>

</Project>  

Compilar la solución

Una vez que tenemos las dependencias descargadas en la carpeta packages del directorio de la solución, podemos crear el paso de la compilación. Este paso se realiza con la tarea MSBuild, donde le indicamos los targets Clean, Build y la configuración Release. Al final tenemos un target nuestro que ejecuta una tarea de MSBuild donde indicamos que ejecute dos targets, a mi me ayuda imaginarme un script de MSBuild como un árbol de targets o las típicas matrioskas rusas.

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

  <Target Name="Default" DependsOnTargets="RestorePackages;Rebuild;Test">
  </Target>

  <Target Name="RestorePackages">
    <Exec Command="&quot;tools\NuGet.exe&quot; restore &quot;TuSolucion.sln&quot;" />
  </Target>

  <Target Name="Rebuild" >
     <MSBuild Targets="Clean;Rebuild" Projects="TuSolucion.sln" Properties="Configuration=Release"/>
  </Target>

  <Target Name="Test">
  </Target>

</Project>  

Ejecutar los tests

Vamos a ver como tenemos que ejecutar los test si fueran de xUnit. Este es el último paso de la compilación.
Al igual que para ejecutar los test de xUnit en Visual Studio tenemos que instalar un runner, para ejecutarlos desde MSBuild necesitamos instalar un runner de MSBuild también en un proyecto de la solución, por ejemplo el de proyecto de test. El paquete que tenemos que descargar es Xunit.runner.msbuild y nos los va a descargar en la carpeta packages del directorio de la solucción.

Ahora tenemos que definir una tarea en nuestro script que ejecute el runner de xunit.
También tenemos que recopilar los proyectos de test con un ItemGroup. Y por último en el target de test ejecutamos la tarea xunit que nos hemos definido y le pasamos los proyectos de test recopilados. Se ejecutará la tarea para cada uno de los proyectos de test.

Asi queda el script final:

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

  <UsingTask
    AssemblyFile="packages\xunit.runner.msbuild.2.1.0\build\portable-net45+win8+wp8+wpa81\xunit.runner.msbuild.dll"
    TaskName="Xunit.Runner.MSBuild.xunit" />

  <ItemGroup>
    <TestAssemblies Include="**\bin\Release\*.test.dll" />
  </ItemGroup>

  <Target Name="Default" DependsOnTargets="RestorePackages;Rebuild;Test">
  </Target>

  <Target Name="RestorePackages">
    <Exec Command="&quot;tools\NuGet.exe&quot; restore &quot;TuSolucion.sln&quot;" />
  </Target>

  <Target Name="Rebuild" >
    <MSBuild Targets="Clean;Rebuild" Projects="TuSolucion.sln" Properties="Configuration=Release"/>

  </Target>

  <Target Name="Test">
    <xunit Assemblies="@(TestAssemblies)" />
  </Target>

</Project>  

Conclusiones

Hay que distinguir el proceso de compilación, del proceso de ejecutarlo periodicamente. Es recomendable cubrir el primer proceso de forma independiente del servidor de integración continua mediante un script de MSBuild que cubra todo el proceso.