Cobertura de código é essencial para sabermos o quanto estamos investindo na qualidade do nosso projeto. Para isso, até temos recursos nativos na IDE do Visual Studio, mas apenas na edição Enterprise. Mas nem tudo está perdido para quem não tem acesso à edições “premium” do VS! Podemos substituir tranquilamente pelo OpenCover, que atende a este necessidade com grande eficiência e elegância. E isso veremos no projeto de exemplo que iremos montar com o Visual Studio 2017 e o NUnit.
Observação: o OpenCover atende outras plataformas de testes, inclusive a nativa da Microsoft, irei o Usar o NUnit apenas por escolha própria, fique a vontade para integrar com a ferramente que convir.
Crie a solução dos projetos
Crie uma solução vazia chamada CodeCoverage, e inclua três Solution Folders chamadas Domain, Presentation e Tests. na pasta Tests, inclua mais duas subpastas chamadas Domain e Presentation, que serão os testes específicos de cada camada.

Projeto de domínio
Crie um projeto do tipo Class Library chamado CodeCoverage.Domain dentro da pasta Domain, e dentro do projeto inclua outra pasta chamada Entities. Nesta pasta inclua três classes chamadas State, City e Person, pois estas classes que serão analisadas pelo OpenCover, afim de gerar um relatório com o percentual de cobertura por testes unitários. A seguir o código de cada uma delas:
Classe de estados
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
using System; namespace CodeCoverage.Domain.Entities { public class State { public State(Guid id, string name) { Id = id; Name = name; } public Guid Id { get; } public string Name { get; } public override string ToString() { return Name; } } } |
Classe de cidades
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
using System; namespace CodeCoverage.Domain.Entities { public class City { public City(Guid id, string name, Guid stateId, State state) { Id = id; Name = name; StateId = stateId; State = state; } public Guid Id { get; } public string Name { get; } public Guid StateId { get; } #region Relationships public virtual State State { get; } #endregion public override string ToString() { return Name; } } } |
Classe de pessoas
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
using System; namespace CodeCoverage.Domain.Entities { public class Person { public Person(Guid id, string name, Guid? cityId, Guid? stateId, City city, State state) { Id = id; Name = name; CityId = cityId; StateId = stateId; City = city; State = state; } public Guid Id { get; } public string Name { get; } public Guid? CityId { get; } public Guid? StateId { get; set; } #region Relationships public virtual City City { get; } public virtual State State { get; } #endregion public override string ToString() { return Name; } } } |
Projeto de Console
O projeto de console, será a nossa camada de apresentação, onde vamos trabalhar com DTOs, que são representações dos dados das nossas classes de domínio. Para isso, na pasta Presentation da solução, adicione um projeto do tipo Console Application chamado CodeCoverage.Prompt, e dentro do projeto inclua um pasta chamada Dto. Onde residirão as 3 classes de dados que serão a StateDto, CityDto e a PersonDto, como segue:
DTO de estado
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
using System; namespace CodeCoverage.Prompt.Dto { public class StateDto { public StateDto(Guid id, string name) { Id = id; Name = name; } public Guid Id { get; } public string Name { get; } public override string ToString() { return Name; } } } |
DTO de cidades
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
using System; namespace CodeCoverage.Prompt.Dto { public class CityDto { public CityDto(Guid id, string name, Guid stateId, StateDto stateDto) { Id = id; Name = name; StateId = stateId; State = stateDto; } public Guid Id { get; } public string Name { get; } public Guid StateId { get; } #region Relationships public virtual StateDto State { get; } #endregion public override string ToString() { return Name; } } } |
DTO de pessoa
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
using System; namespace CodeCoverage.Prompt.Dto { public class PersonDto { public PersonDto(Guid id, string name, Guid? stateId, Guid? cityId, CityDto cityDto, StateDto stateDto) { Id = id; Name = name; StateId = stateId; CityId = cityId; CityDto = cityDto; StateDto = stateDto; } public Guid Id { get; } public string Name { get; } public Guid? CityId { get; } public Guid? StateId { get; set; } #region Relationships public virtual CityDto CityDto { get; } public virtual StateDto StateDto { get; } #endregion public override string ToString() { return Name; } } } |
Edite a classe Program.cs e crie o método que faz os mapeamento entre o Domínio e DTO e o código que exibe os dados em tela.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
using System; using CodeCoverage.Domain.Entities; using CodeCoverage.Prompt.Dto; namespace CodeCoverage.Prompt { internal class Program { private static StateDto _stateDto; private static CityDto _cityDto; private static PersonDto _personDto; private static void Main() { var state = new State(Guid.NewGuid(), "RS"); var city = new City(Guid.NewGuid(), "Porto Alegre", state.Id, state); var person = new Person(Guid.NewGuid(), "Tiago", city.Id, state.Id, city, state); SetMappings(person, city, state); Console.WriteLine($"Name: {_personDto.Name}"); Console.WriteLine($"City: {_personDto.CityDto.Name}"); Console.WriteLine($"State: {_personDto.StateDto.Name}"); Console.ReadKey(); } private static void SetMappings(Person person, City city, State state) { _stateDto = new StateDto(state.Id, state.Name); _cityDto = new CityDto(city.Id, city.Name, city.StateId, _stateDto); _personDto = new PersonDto(person.Id, person.Name, person.StateId, person.CityId, _cityDto, _stateDto); } } } |
Observação: não é intenção deste artigo explicar como funciona processos de mapeamentos entre entidades de domínio, DTOs e ViewModels. Mas fica a dica para você procurar na internet qual o funcionamento de um AutoMapper, por exemplo.
Testes
Expanda a pasta de solução Tests e dentro da pasta Domain adicione um projeto do tipo Unit Test Project chamado CodeCoverage.Domain.Tests.

Neste projeto, inclua uma pasta chamada Entities, e dentro dela vamos incluir duas classes de testes, mas antes é preciso instalar o pacote NUnit, para isso abra o Package Manager Console selecione o projeto de testes do domínio e digite:

Adicione uma referência para o projeto de domínio, para que os testes possam acessar as entidades. Então adicione as classes de testes na pasta Entities.
Testes da classe de estado
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
using System; using CodeCoverage.Domain.Entities; using NUnit.Framework; namespace CodeCoverage.Domain.Tests.Entities { [TestFixture] public class StateTests { [Test] public void VerifyName() { var sut = new State(Guid.NewGuid(), "Test name"); Assert.AreEqual("Test name", sut.Name); } } } |
Testes da classe de pessoa
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
using System; using CodeCoverage.Domain.Entities; using NUnit.Framework; namespace CodeCoverage.Domain.Tests.Entities { [TestFixture] public class PersonTests { [Test] public void VerifyName() { var sut = new Person(Guid.NewGuid(), "Test name", null, null, null, null); Assert.AreEqual("Test name", sut.Name); } [Test] public void VerifyId() { var sut = new Person(Guid.NewGuid(), "Test name", null, null, null, null); Assert.AreNotEqual(Guid.Empty, sut.Id); } } } |
Adicione um outro projeto do tipo Unit Test Project chamado CodeCoverage.Prompt.Tests e adicione uma referência do projeto CodeCoverage.Prompt e instale o pacote do NUnit também.
Crie uma pasta chamada Dto, e dentro dela inclua uma classe de testes chamada PersonDtoTests para pessoas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
using System; using CodeCoverage.Prompt.Dto; using NUnit.Framework; namespace CodeCoverage.Prompt.Tests.Dto { [TestFixture] public class PersonDtoTests { [Test] public void VerifyName() { var sut = new PersonDto(Guid.NewGuid(), "Test name", null, null, null, null); Assert.AreEqual("Test name", sut.Name); } } } |
Usando o Cake
Abra o Powershell, e se posicione na pasta da solução, e logo após digite o comando para instalar o pacote que compila e executa o script do Cake:
|
Invoke-WebRequest https://cakebuild.net/download/bootstrapper/windows -OutFile build.ps1 |
Ainda na pasta raiz da solução, crie um novo arquivo chamado build.cake e crie também a pasta docs/testsResults/Reports, que será onde ficará os resultados dos testes.
Dica: você pode adicionar o arquivo build.cake à solução, para que ele faça parte do projeto, mas é muito mais produtivo usar o Visual Studio Code e a extensão do Cake, conforme expliquei neste artigo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
#tool "nuget:?package=OpenCover" #tool "nuget:?package=NUnit.ConsoleRunner" #tool "nuget:?package=ReportGenerator" var target = Argument("target", "Default"); Task("BuildProjects") .Does(() => { foreach(var project in GetFiles("./src/**/*.csproj")) { MSBuild(project.GetDirectory().FullPath, new MSBuildSettings { Verbosity = Verbosity.Minimal, Configuration = "Debug" } ); } }); Task("BuildTests") .IsDependentOn("BuildProjects") .Does(() => { foreach(var test in GetFiles("./tests/**/*.csproj")) { MSBuild(test.GetDirectory().FullPath, new MSBuildSettings { Verbosity = Verbosity.Minimal, Configuration = "Debug" } ); } }); Task("OpenCover") .IsDependentOn("BuildTests") .Does(() => { var openCoverSettings = new OpenCoverSettings() { Register = "user", SkipAutoProps = true, ArgumentCustomization = args => args.Append("-coverbytest:*.Tests.dll").Append("-mergebyhash") }; var outputFile = new FilePath("./docs/testsResults/Reports/CodeCoverageReport.xml"); OpenCover(tool => { var testAssemblies = GetFiles("./tests/**/bin/Debug/*.Tests.dll"); tool.NUnit3(testAssemblies); }, outputFile, openCoverSettings .WithFilter("+[CodeCoverage*]*") .WithFilter("-[CodeCoverage.*.Tests]*") ); }); Task("ReportGenerator") .IsDependentOn("OpenCover") .Does(() =>{ var reportGeneratorSettings = new ReportGeneratorSettings() { HistoryDirectory = new DirectoryPath("./docs/testsResults/Reports/ReportsHistory") }; ReportGenerator("./docs/testsResults/Reports/CodeCoverageReport.xml", "./docs/testsResults/Reports/ReportGeneratorOutput", reportGeneratorSettings); }); Task("Default") .IsDependentOn("ReportGenerator") .Does(() => { if (IsRunningOnWindows()) { var reportFilePath = ".\\docs\\testsResults\\Reports\\ReportGeneratorOutput\\index.htm"; StartProcess("explorer", reportFilePath); } }); RunTarget(target); |
Volte ao Powershell e digite o comando .\build.ps1 para executar o script do Cake.

Após a execução do script, o Report Generator irá compilar os arquivos xml do OpenCover e gerar uma visualização mais amigável e detalhada e ainda abrir um sumário no seu navegador padrão, incluind um histório de cobertura.

.gitignore
Se você está usando o arquivo .gitignore padrão do Visual Studio – aquele que é fornecido, por exemplo, pelo GitHub ou Visual Studio Online – será preciso alterar para que ele ignore os arquivos compilados do Cake e não suba para o repositório do GitHub. Para isso encontre o trecho a seguir:
|
# Cake - Uncomment if you are using it # tools/** # !tools/packages.confi |
E descomente as duas últimas linhas.
|
# Cake - Uncomment if you are using it tools/** !tools/packages.config |
Pronto, agora podemos acompanhar a evolução dos testes de nosso aplicativo de forma elegante e segura.
Até a próxima e se quiser, acesse o projeto completo em meu GitHub:
https://github.com/tiagopariz/CodeCoverage