Integração contínua para um projeto .NET Core 3.0 usando TravisCI

· 10 min de leitura

Neste último fim de semana decidi que queria iniciar corretamente meu projeto scraper-checker-downloader que venho fazendo em diferentes repositórios.

Depois de iniciar mais um projeto, isso tinha que ser legal, muito legal, usando CI/CD, pull request, documentação, badges no readme, tudo que eu vi que é legal e de fato são as melhores práticas.

E depois de um bom final de semana acabei criando o Dramarr, um conjunto de ferramentas que fazem scrap e baixam programas de diversas fontes.

Possui diferentes repositórios na organização e a maioria deles são bibliotecas que estão sendo compiladas, testadas e implantadas no pull request e quando são mescladas no branch master.

Isso é chamado de CI/CD ou integração contínua/entrega contínua.

Mas neste tutorial falaremos apenas sobre CI.

Integração contínua

O que é isso?

Retirado do [blog](https://martinfowler.com/articles/continuousIntegration.html de Martin Fowler), que é a melhor explicação que li:

Integração Contínua é uma prática de desenvolvimento de software onde os membros de uma equipe integram seu trabalho com frequência, geralmente cada pessoa integra pelo menos diariamente - levando a múltiplas integrações por dia. Cada integração é verificada por uma construção automatizada (incluindo teste) para detectar erros de integração o mais rápido possível.

Ferramentas

Existem muitas ferramentas para integrar seu fluxo de trabalho com CI/CD, mas para este tutorial usaremos Github para armazenar nosso código e as ferramentas TravisCI para configurar o CI. Em relação à linguagem e frameworks, utilizaremos C# e o novo .NET Core 3.0.

Requisitos

Para fazer isso funcionar, você precisa de três coisas simples:

  1. Versão mais recente do Visual Studio 2019
  2. Conta Github
  3. Conta Travis-CI vinculada à sua conta Github

Projeto

Para o propósito deste tutorial, faremos uma calculadora simples. Estaremos criando uma biblioteca, uma ferramenta de linha de comando e um projeto de teste para testar tudo.

Este projeto de teste também estará em execução quando configurarmos o CI, o que significa que se no futuro fizermos uma alteração no código e os testes que criamos inicialmente não passarem, receberemos uma notificação ou poderemos simplesmente rejeitar o pull request.

Criando o repositório Github

Primeiro, criaremos um repositório Github, então vá até o Github, crie o repositório e clone-o em seu ambiente local. Decidi chamar este novo repositório de CalculatorCLI-demo.

Criando a solução

Agora vamos criar uma solução vazia chamada CalculatorCLI, na pasta raiz do repositório clonado

Biblioteca principal

Como seria em um projeto do mundo real, armazenaremos nossa lógica em um projeto separado que gera uma biblioteca, então vamos criá-la.

Vá e crie um Class Library (.NET Standard) e nomeie-o como CalculatorCLI.Core

Versão do NET CoreAssim que você criar o projeto, acesse as propriedades do projeto e altere o Target framework para .NET Standard 2.1, para torná-lo compatível com projetos construídos em .NET Core 3.0.

Código

Para fins de tutorial, vamos criar uma classe simples que lida com operações.

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

Agora que temos o projeto principal, vamos criar o aplicativo. Neste caso, será um aplicativo de console simples que aceita argumentos e mostra um resultado.

Então vamos em frente e criar um novo Console App (.NET Core), eu o nomeei CalculatorCLI.CLI.

Versão do NET Core

Assim como fizemos antes, assim que você criar o projeto, vá nas propriedades do projeto e altere o Target framework para .NET Core 3.0, caso ainda não esteja assim.

Em seguida, adicione a referência ao ConsoleCLI.Core ao nosso projeto recém-criado.

Código

Agora, para o código, isso é mais simples do 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");
        }
    }
}

Estaremos utilizando esta aplicação a partir de um comando, então para que funcione temos que chamá-la passando alguns parâmetros. Por exemplo:

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

O que se traduz em:

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

O código para isso é bem simples, se não corresponder a um determinado padrão regex, é uma chamada errada e chama o PrintUsage(). Isso significa que se inserirmos algo diferente de um número, por estar definido na regex, ele nem tentará fazer o cálculo.

Isso significa que, se chamarmos assim:

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

Ele nunca entrará na lógica de operações e estamos salvando verificações futuras, como TryParse dos valores.

Teste

Temos a biblioteca principal e a linha de comando, mas precisamos testar agora, porque é isso que queremos fazer no CI.

Então, vamos criar um novo MSTest Test Project (.NET Core) e nomeá-lo CalculatorCLI.Tests.

Versão do NET Core

Assim como fizemos antes, assim que você criar o projeto, vá nas propriedades do projeto e altere o Target framework para .NET Core 3.0, caso ainda não esteja assim.

Em seguida, adicione a referência a ConsoleCLI.Core e ConsoleCLI.Core ao nosso projeto de teste recém-criado.

Código

Vamos dividir o teste em dois arquivos diferentes: CoreTests.cs e 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);
        }
    }
}

Com tudo criado terminaremos com uma solução como esta:

E com isso agora podemos executar os Testes, então vá até o Test Explorer no Visual Studio e execute-os!

#Travis CI

Se ainda não o fez, TravisCI é um sistema hospedado de integração e implantação contínua.

Existem alguns passos que precisamos seguir aqui, mas primeiro vamos vincular nosso repositório Github para ser ouvido pelos agentes TravisCI para construir e testar nosso projeto.

Habilitar repositório

Para fazer isso, faça login na página do Travis CI e vá até seus repositórios, depois filtre o projeto que você criou e habilite-o, clicando no controle deslizante ao lado do nome do repositório.

Crie .travis.ymlPrecisamos criar um arquivo chamado .travis.yml na raiz do seu projeto, isso porque conforme indicado na documentação:

O Travis só executa compilações nos commits que você envia depois de adicionar um arquivo .travis.yml.

Então crie um arquivo .travis.yml na raiz do repositório com as seguintes linhas:

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

Não vou entrar na sintaxe de como o arquivo .travis.yml funciona, mas vamos revisar o que ele está fazendo:

  1. Configuramos que o idioma será csharp.
  2. Não usaremos mono porque .NET Core 3.0 será executado nativamente no Linux.
  3. Definimos a versão dotnet como 3.0.
  4. Definimos o os, por padrão é linux, mas eu adicionei mesmo assim.
  5. Agora temos before_script que irá funcionar antes da lógica principal .aqui, então o que coloquei foi rodar dotnet restore para a solução para que tudo carregue perfeitamente depois.
  6. Agora no script, faremos um dotnet builld e dotnet test em nossa solução, isso irá verificar se ela compila e então executar os testes.

Eae terminamos!

Carregar para mestre

Agora só precisamos empurrar tudo para master.

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

Verifique a integração contínua

Podemos verificar o status do CI do push para master que fizemos na página do repositório ou no painel do TravisCI.

Em progresso



Concluído



#Vamos quebrar isso

Agora, para ver o quão poderoso isso é, vamos quebrar o código e alterar a biblioteca principal para fazer com que ela falhe.

Mudanças de código

Então vá para o Operation.cs e mude algo que irá quebrar alguns testes.

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");
            }
        }
    }
}

E se executarmos o teste novamente, porque alteramos o caso para adição, ele falhará:

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

Como esperado, falhou no caso ShouldAdd:

Agora faça um commit dessa mudança e envie-a para o master, e aguarde os resultados do agente TravisCI.

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

Construir

Agora vamos para os logs do TravisCI e veremos que quebramos o projeto com sucesso, pois os testes de integração estão falhando e o status da compilação é erro.

No final do log podemos ver o próprio erro:

Vamos consertar de novo!

Agora reverta o que fizemos e envie o código para master e verifique o status da nova compilação.

Os testes estão passando com sucesso:

E construir também é um sucesso:

Conclusão

É realmente muito poderoso, CI e CD já existem há muito tempo, mas agora é muito simples executá-lo em cada projeto, não importa quão pequeno ou simples seja.Do meu ponto de vista, todos deveriam pelo menos configurar o CI para cada um de seus projetos, porque é uma boa prática e eventualmente economizará tempo na depuração e na localização de erros que não deveriam ocorrer se você tivesse definido tests e CI adequados.

#É isso

É isso sobre como criar uma solução .NET Core 3.0 que tenha integração contínua em cada build usando TravisCI e armazenando o código no Github.

Você pode encontrar o código-fonte deste projeto aqui.