Integração contínua para um projeto .NET Core 3.0 usando TravisCI
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:
- Versão mais recente do Visual Studio 2019
- Conta Github
- 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:
- Configuramos que o idioma será
csharp. - Não usaremos
monoporque.NET Core 3.0será executado nativamente no Linux. - Definimos a versão
dotnetcomo3.0. - Definimos o
os, por padrão élinux, mas eu adicionei mesmo assim. - Agora temos
before_scriptque irá funcionar antes da lógica principal .aqui, então o que coloquei foi rodardotnet restorepara a solução para que tudo carregue perfeitamente depois. - Agora no
script, faremos umdotnet builldedotnet testem 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.