Integración continua para un proyecto .NET Core 3.0 usando TravisCI

· 10 min de lectura

El fin de semana pasado decidí que quería comenzar correctamente mi proyecto scraper-checker-downloader que he estado haciendo en diferentes repositorios.

Después de comenzar otro proyecto más, esto tenía que ser genial, realmente genial, usando CI/CD, solicitud de extracción, documentación, insignias en el archivo Léame, todo lo que he visto es genial y, de hecho, son las mejores prácticas.

Y después de un buen fin de semana terminé creando Dramarr, un conjunto de herramientas que eliminan y descargan programas de diferentes fuentes.

Tiene diferentes repositorios en la organización y la mayoría de ellos son bibliotecas que se compilan, prueban e implementan en la solicitud de extracción y cuando se fusionan en la rama maestra.

Eso es lo que se llama CI/CD o integración continua/entrega continua.

Pero en este tutorial solo hablaremos de CI.

Integración continua

¿Qué es?

Tomado del [blog] de Martin Fowler (https://martinfowler.com/articles/continuousIntegration.html), que es la mejor explicación que he leído:

La Integración Continua es una práctica de desarrollo de software en la que los miembros de un equipo integran su trabajo con frecuencia; por lo general, cada persona se integra al menos diariamente, lo que genera múltiples integraciones por día. Cada integración se verifica mediante una compilación automatizada (incluida la prueba) para detectar errores de integración lo más rápido posible.

Herramientas

Hay muchas herramientas para integrar su flujo de trabajo con CI/CD, pero para este tutorial usaremos Github para almacenar nuestro código y las herramientas TravisCI para configurar el CI. En cuanto al lenguaje y frameworks, usaremos C# y el nuevo .NET Core 3.0.

Requisitos

Para que esto funcione necesitas tres cosas simples:

  1. Última versión de Visual Studio 2019
  2. Cuenta Github
  3. Cuenta Travis-CI vinculada a su cuenta Github

Proyecto

Por el bien de este tutorial, haremos una Calculadora simple. Crearemos una biblioteca, una herramienta de línea de comandos y un proyecto de prueba para probar todo.

Este proyecto de prueba también se ejecutará cuando configuremos el CI, lo que significa que si en el futuro realizamos un cambio en el código y las pruebas que creamos inicialmente no pasan, recibiremos una notificación o simplemente podríamos rechazar la solicitud de extracción.

Creando el repositorio de Github

Primero, crearemos un repositorio de Github, así que acceda a Github, cree el repositorio y clónelo en su entorno local. Decidí llamar a este nuevo repositorio CalculatorCLI-demo.

Creando la solución

Ahora creemos una solución vacía llamada CalculatorCLI, en la carpeta raíz del repositorio clonado.

Biblioteca principal

Como sería en un proyecto del mundo real, almacenaremos nuestra lógica en un proyecto separado que genera una biblioteca, así que vamos a crearla.

Ve y crea un Class Library (.NET Standard) y llámalo CalculatorCLI.Core

Versión NET CoreTan pronto como cree el proyecto, vaya a las propiedades del proyecto y cambie Target framework a .NET Standard 2.1, para que sea compatible con los proyectos creados en .NET Core 3.0.

Código

Por el bien del tutorial, creemos una clase simple que maneje operaciones.

using System;

namespace ConsoleCalculator.Core
{
    public enum OperatorsEnum
    {
        ADD,
        SUBSTRACT,
        MULTIPLY,
        DIVIDE
    }

    public class Operation
    {
        public OperatorsEnum OperatorEnum { get; set; }
        public int LeftValue { get; set; }
        public int RightValue { get; set; }

        public Operation(string operatorString, int leftValue, int rightValue)
        {
            switch (operatorString)
            {
                case "+":
                    OperatorEnum = OperatorsEnum.ADD;
                    break;
                case "-":
                    OperatorEnum = OperatorsEnum.SUBSTRACT;
                    break;
                case "*":
                    OperatorEnum = OperatorsEnum.MULTIPLY;
                    break;
                case "/":
                    OperatorEnum = OperatorsEnum.DIVIDE;
                    break;
                default:
                    throw new Exception("Operator invalid");
            }

            LeftValue = leftValue;
            RightValue = rightValue;
        }

        public int DoOperation()
        {
            switch (OperatorEnum)
            {
                case OperatorsEnum.ADD:
                    return LeftValue + RightValue;
                case OperatorsEnum.SUBSTRACT:
                    return LeftValue - RightValue;
                case OperatorsEnum.MULTIPLY:
                    return LeftValue * RightValue;
                case OperatorsEnum.DIVIDE:
                    return LeftValue / RightValue;
                default:
                    throw new Exception("Operator is not valid");
            }
        }
    }
}

##CLI

Ahora que tenemos el proyecto principal, creemos la aplicación. En este caso será una aplicación de consola simple que acepta argumentos y muestra un resultado.

Así que sigamos adelante y creemos un nuevo Console App (.NET Core), lo llamé CalculatorCLI.CLI.

Versión NET Core

Como hicimos antes, tan pronto como crees el proyecto, ve a las propiedades del proyecto y cambia Target framework a .NET Core 3.0, si aún no está así.

Luego agregue la referencia a ConsoleCLI.Core a nuestro proyecto recién creado.

Código

Ahora el código, esto es más simple que antes.

using ConsoleCalculator.Core;
using System;
using System.Text.RegularExpressions;

namespace ConsoleCalculator.CLI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                PrintUsage();
            }
            else
            {
                var joinedArgs = string.Join(" ", args);
                var regex = @"-op [\+\-\*\/] -l [-0-9]+ -r [-0-9]+";

                if (Regex.IsMatch(joinedArgs, regex))
                {
                    int _left = Int32.Parse(args[3]);
                    int _right = Int32.Parse(args[5]);
                    string _operator = args[1];

                    var _operation = new Operation(_operator, _left, _right);
                    var _result = _operation.DoOperation();

                    Console.WriteLine($"Result is: {_result}");
                }
                else
                {
                    PrintUsage();
                }
            }
        }

        public static void PrintUsage()
        {
            Console.WriteLine($"Welcome to ConsoleCalculator!");
            Console.WriteLine($"");
            Console.WriteLine($"-op Operator, it must be +,-,*,/");
            Console.WriteLine($"-l Left number");
            Console.WriteLine($"-r Left number");
            Console.WriteLine($"");
            Console.WriteLine($"Example usage: -op + -l 5 -r 6");
        }
    }
}

Usaremos esta aplicación desde un comando como, por lo que para que funcione, debemos llamarla pasando algunos parámetros. Por ejemplo:

ConsoleCalculator.CLI.exe -op + -l 10 -r 20

Lo que se traduce en:

ConsoleCalculator.CLI.exe -operator + -leftValue 10 -rightValue 20

El código para esto es bastante simple, si no coincide con un determinado patrón de expresiones regulares, es una llamada incorrecta y llama a PrintUsage(). Esto significa que si ingresamos algo diferente a un número, porque está configurado en la expresión regular, ni siquiera intentará realizar el cálculo.

Eso significa, si lo llamamos así:

ConsoleCalculator.CLI.exe -operator + -leftValue asdfg -rightValue ghjk

Nunca entrará dentro de la lógica de operaciones y estamos guardando comprobaciones futuras como TryParseing los valores.

Prueba

Tenemos la biblioteca principal y la línea de comando, pero ahora necesitamos realizar pruebas, porque eso es lo que queremos hacer en el CI.

Así que sigamos adelante y creemos un nuevo MSTest Test Project (.NET Core) y asígnale el nombre CalculatorCLI.Tests.

Versión NET Core

Como hicimos antes, tan pronto como crees el proyecto, ve a las propiedades del proyecto y cambia Target framework a .NET Core 3.0, si aún no está así.

Luego agregue la referencia a ConsoleCLI.Core y ConsoleCLI.Core a nuestro proyecto de prueba recién creado.

Código

Vamos a dividir la prueba en dos archivos diferentes: CoreTests.cs y CLITests.cs

using CalculatorCLI.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Text;

namespace CalculatorCLI.Tests
{
    [TestClass]
    public class CoreTests
    {
        public int _left = 2;
        public int _right = 2;

        [TestMethod]
        public void ShouldAdd()
        {
            var expectedResult = 4;
            var operation = new Operation("+", _left, _right);
            var functionResult = operation.DoOperation();

            Assert.AreEqual(functionResult, expectedResult);
        }

        [TestMethod]
        public void ShouldSubstract()
        {
            var expectedResult = 0;
            var operation = new Operation("-", _left, _right);
            var functionResult = operation.DoOperation();

            Assert.AreEqual(functionResult, expectedResult);
        }

        [TestMethod]
        public void ShouldMultiply()
        {
            var expectedResult = 4;
            var operation = new Operation("*", _left, _right);
            var functionResult = operation.DoOperation();

            Assert.AreEqual(functionResult, expectedResult);
        }

        [TestMethod]
        public void ShouldDivide()
        {
            var expectedResult = 1;
            var operation = new Operation("/", _left, _right);
            var functionResult = operation.DoOperation();

            Assert.AreEqual(functionResult, expectedResult);
        }

        [TestMethod]
        [ExpectedException(typeof(System.DivideByZeroException))]
        public void ShouldThrowExceptionForDivideByZero()
        {
            var operation = new Operation("/", _left, 0);
            operation.DoOperation();
        }

        [TestMethod]
        [ExpectedException(typeof(System.Exception), "Operator invalid")]
        public void ShouldThrowExceptionForWrongOperator()
        {
            var operation = new Operation("text", _left, 0);
            operation.DoOperation();
        }
    }
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Text;

namespace CalculatorCLI.Tests
{
    [TestClass]
    public class CLITests
    {
        public string _left = "2";
        public string _right = "2";

        [TestMethod]
        public void ShouldAdd()
        {
            var args = new string[] { "-op", "+", "-l", "45", "-r", "96" };
            CalculatorCLI.CLI.Program.Main(args);
        }

        [TestMethod]
        public void ShouldSubstract()
        {
            var args = new string[] { "-op", "-", "-l", "45", "-r", "96" };
            CalculatorCLI.CLI.Program.Main(args);
        }

        [TestMethod]
        public void ShouldMultiply()
        {
            var args = new string[] { "-op", "*", "-l", "45", "-r", "96" };
            CalculatorCLI.CLI.Program.Main(args);
        }

        [TestMethod]
        public void ShouldDivide()
        {
            var args = new string[] { "-op", "/", "-l", "45", "-r", "96" };
            CalculatorCLI.CLI.Program.Main(args);
        }
    }
}

Con todo creado terminaremos con una solución como esta:

Y con esto ahora podemos ejecutar las Pruebas, así que vaya a Test Explorer en Visual Studio y ejecútelas.

#TravisCI

Si aún no lo ha hecho, TravisCI es un sistema alojado de integración e implementación continua.

Hay algunos pasos que debemos seguir aquí, pero primero vamos a vincular nuestro repositorio de Github para que los agentes de TravisCI lo escuchen y puedan construir y probar nuestro proyecto.

Habilitar repositorio

Para hacer esto, inicie sesión en la página de Travis CI y vaya a sus repositorios, luego filtre el proyecto que creó y habilítelo haciendo clic en el control deslizante al lado del nombre del repositorio.

Crear .travis.ymlNecesitamos crear un archivo llamado .travis.yml en la raíz de su proyecto, esto se debe a que como se indica en la documentación:

Travis solo ejecuta compilaciones de las confirmaciones que envía después de haber agregado un archivo .travis.yml.

Así que ve y crea un archivo .travis.yml en la raíz del repositorio con las siguientes líneas:

language:
    csharp
sudo: required
mono: none 
dotnet: 3.0

os:
  - linux

before_script:
    - dotnet restore ".\CalculatorCLI\CalculatorCLI.sln"
    
script:
    - dotnet build ".\CalculatorCLI\CalculatorCLI.sln" -c Release
    - dotnet test ".\CalculatorCLI\CalculatorCLI.sln" -c Release -v n

No entraré en la sintaxis de cómo funciona el archivo .travis.yml, pero repasemos lo que hace:

  1. Configuramos que el idioma será csharp.
  2. No usaremos mono porque .NET Core 3.0 se ejecutará de forma nativa en Linux.
  3. Configuramos la versión dotnet en 3.0.
  4. Configuramos el os, por defecto es linux pero lo agregué de todos modos.
  5. Ahora tenemos before_script que se ejecutará antes de la lógica principal aquí, así que lo que puse fue ejecutar dotnet restore en la solución para que todo se cargue perfectamente después.
  6. Ahora en el script, haremos un dotnet builld y un dotnet test a nuestra solución, esto comprobará que se compila y luego ejecutará las pruebas.

¡Y hemos terminado!

Subir al master

Ahora sólo tenemos que impulsar todo para dominarlo.

git add --all
git commit -m "Initial files"
git push

Verifique la integración continua

Podemos verificar el estado de CI del envío a master que hicimos tanto en la página del repositorio como en el panel de TravisCI.

En progreso



Terminado



Vamos a romperlo

Ahora, para ver qué tan poderoso es esto, rompamos el código y cambiemos la biblioteca principal para que falle.

Cambios de código

Así que vaya a Operation.cs y cambie algo que rompa algunas pruebas.

using System;

namespace CalculatorCLI.Core
{
    public enum OperatorsEnum
    {
        ADD,
        SUBSTRACT,
        MULTIPLY,
        DIVIDE
    }

    public class Operation
    {
        public OperatorsEnum OperatorEnum { get; set; }
        public int LeftValue { get; set; }
        public int RightValue { get; set; }

        public Operation(string operatorString, int leftValue, int rightValue)
        {
            switch (operatorString)
            {
                case "+":
                    OperatorEnum = OperatorsEnum.SUBSTRACT;
                    break;
                case "-":
                    OperatorEnum = OperatorsEnum.SUBSTRACT;
                    break;
                case "*":
                    OperatorEnum = OperatorsEnum.MULTIPLY;
                    break;
                case "/":
                    OperatorEnum = OperatorsEnum.DIVIDE;
                    break;
                default:
                    throw new Exception("Operator invalid");
            }

            LeftValue = leftValue;
            RightValue = rightValue;
        }

        public int DoOperation()
        {
            switch (OperatorEnum)
            {
                case OperatorsEnum.ADD:
                    return LeftValue + RightValue;
                case OperatorsEnum.SUBSTRACT:
                    return LeftValue - RightValue;
                case OperatorsEnum.MULTIPLY:
                    return LeftValue * RightValue;
                case OperatorsEnum.DIVIDE:
                    return LeftValue / RightValue;
                default:
                    throw new Exception("Operator is not valid");
            }
        }
    }
}

Y si ejecutamos la prueba nuevamente, porque cambiamos el caso a suma, fallará:

case "+":
    OperatorEnum = OperatorsEnum.SUBSTRACT;
    break;

Como era de esperar, falló en el caso ShouldAdd:

Ahora confirme este cambio, empújelo a master y espere los resultados del agente TravisCI.

git add --all
git commit -m "Breaking changes"
git push

Construir

Ahora vayamos a los registros de TravisCI y veremos que rompimos el proyecto con éxito, porque la prueba de integración está fallando y el estado de compilación es error.

Al final del registro podemos ver el error en sí:

¡Arreglemoslo de nuevo!

Ahora revierta lo que hicimos, envíe el código a Master y verifique el estado de la nueva compilación.

Las pruebas se pasan con éxito:

Y construir también es un éxito:

Conclusión

Es realmente bastante poderoso, CI y CD existen desde hace mucho tiempo, pero ahora es bastante sencillo ejecutarlo en cada proyecto, no importa cuán pequeño o simple sea.Desde mi punto de vista, todos deberían al menos configurar CI para cada uno de sus proyectos, porque es una buena práctica y eventualmente le ahorrará tiempo depurando y encontrando errores que no deberían ocurrir si hubiera configurado tests y CI adecuados.

Eso es todo

Se trata de cómo crear una solución .NET Core 3.0 que tenga integración continua en cada compilación usando TravisCI y almacenando el código en Github.

Puede encontrar el código fuente de este proyecto aquí.