Por que "tudo junto" é separado e "separado" é tudo junto? Ou... divagações sobre estrutura de testes em .NET
Published: 2024-02-09 19:30:45

**YAN JUSTINO**
[MSc. Software Engineering](https://repositorio.ufrn.br/handle/123456789/26370) · [PhD. Student](https://www.cesar.school/doutorado-profissional-em-engenharia-de-software/)
[AWS](https://www.youracclaim.com/users/yan-justino/badges) · [MCSD](https://www.youracclaim.com/users/yan-justino/badges) · [OCA](https://www.youracclaim.com/users/yan-justino/badges) · [ORCID](https://orcid.org/0000-0001-7248-716X)  · [Tech Lead at ITAÚ Unibanco]()

![Static Badge](https://img.shields.io/badge/demo-tests-F6A506?style=for-the-badge)

---
_Atualizado em 10/02/2024_

O **Design de Software** é uma disciplina que procura diminuir a carga cognitiva entre software e os problemas do "mundo real". Muitos conceitos e técnicas podem ser aplicados ao código a fim de se alcançar essa capacidade. Partindo da perspectiva de um desenvolvedor, penso que o design pode se manifestar como uma espécie de "estética" que evidência um fluxo coeso da informação, estruturado em uma visão de solução. Particularmente, gosto quando o código transmite aquele senso de "beleza" que nos faz exclamar: 

> _Uau! Que código bem escrito!!!!_

Focando em um recorte específico da organização de código, tenho, por experiência, a impressão de que a qualidade estética aplicada aos artefatos do código de produção (_src_) não é proporcional a aplicada aos artefatos de testes. Revisitando o passado, percebo que em diversos projetos que pude atuar, havia uma certa falta de "apreço" com a escrita e manutenção dos testes. 

Independentemente da sênioridade dos desenvolvedores, e da relevância do projeto, os testes seguiam seu destino cruel em uma jornada rumo a "cafonice". Recorrendo a um _MEA CULPA_, eu também fui responsável ao longo de minha carreira, por escrever diversas bizarrices em forma de testes (Quem nunca?). Diante disso, uma pulga atrás orelha me faz pensar na seguinte questão:

> Como aspectos culturais somados a algumas estratégias de design impactam a evolução do código de teste?

Essa não é uma pergunta simples e talvez mereça uma investigação mais profunda do que essas divagações. Guiado por um impulso extremamente pragmático, direciono a atenção para algumas estratégias de design para entender: 

> Como as linguagens e frameworks organizam seus softwares de modo a facilitar a manutenção concomitante do código de produção e do código de testes?

Diante disso, me deparei com um tradeoff bem interessante: **distanciamento vs aproximação "física" entre o código de testes e o código de produção**.

## Distanciamento vs Aproximação

Enquanto desenvolvedor _dotnet_, frequentemente atuo em sistemas que adotam como estratégia o uso de projetos distintos dentro da mesma solução para separar os testes do código de produção. Acredito que essa seja a prática mais comum e [recomendada](https://gist.github.com/davidfowl/ed7564297c61fe9ab814) no mundo dotnet. O modelo a seguir ilustra um exemplo de estrutura de solução dotnet.

```
$/
├─ src/
│  ├─ Production.csproj
│     ├─ ProductionCode.cs
├─ tests/
│  ├─ UnitTests.csproj
│     ├─ TesteCode.cs
├─ .gitignore
├─ README.md
├─ Solution.sln

```

Contudo, tomando por referência outras Linguagens e Frameworks percebe-se uma abordagem diferente: o código de teste é colocado próximo (na mesma pasta ou arquivo) ao o código de produção, como comumente feito em [Go](https://go.dev/doc/modules/layout#basic-package) e [Rust](https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html). O modelo a seguir ilustra como um pacote básico do Go é estruturado.

```
$/
├─ src/
│  ├─ go.mod
│  ├─ modname.go
│  ├─ modname_test.go
├─ .gitignore
├─ README.md

```

Vasculhando a rede sobre essa abordagem, percebi um certo pensamento comum entre seus defensores: de que juntar o código de teste ao código de produção pode oferece algumas vantagens para o processo de desenvolvimento e a manutenibilidade do código. Dentre esses benefícios, eles alegam que a abordagem promove: (a) facilidade de Localização, (b) encorajamento a escrita de testes, (c) colaboração, (d) rápida refatoração, (e) estrutura uniforme etc.  

## Tá... Mas e o C#?

Diante dos potênciais benefícios em aproximar os arquivos de testes com os arquivos de produção, podemos nos perguntar 

> Quais seriam os ganhos e limitações dessa abordagem aplicada em uma solução dotnet? 

Tendo essa questão em mente, convido a darmos um passo desprendido de qualquer pré-conceito para aguçar nosso senso de curiosidade. Para isso, nosso ponto de partida será aplicar a abordagem de unificar o arquivo de testes ao código de produção em um mesmo projeto dotnet. As etapas a seguir detalharão como faremos isso.

### 1. Criando a solução

Diferentemente, de uma solução prototípica em dotnet onde teríamos um projeto de testes apartado, optamos por não criá-lo, e passamos a escrever nossos arquivos de testes diretamente no projeto de produção, fazendo uso da convenção `*.Test.cs`.

```
$/
├─ src/
│  ├─ OrderManagement.csproj/
│     ├─ Order/
│        ├─ OrderItem.cs
│        ├─ OrderItem.Test.cs
├─ .gitignore
├─ README.md
├─ Solution.sln

```

### 2. Configurando o projeto

O próximo passo é adicionar ao projeto, os pacotes relativos a execução dos testes. Nesse ponto, o projeto recebeu as referências aos os pacotes do framework _xUnit_, para executar os testes; e o FluentAssertions, para proporcionar uma maior expressividade as validações. como ilustrar o código de configuração a seguir.

```xml
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    

    <ItemGroup Label="Test Dependecies">
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
        <PackageReference Include="xunit" Version="2.4.2"/>
        <PackageReference Include="FluentAssertions" Version="6.12.0" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="coverlet.collector" Version="6.0.0">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
    </ItemGroup>
</Project>
```

### 3. Escrevendo o Teste de Unidade

Criamos um arquivo chamado `OrdemItem.cs` contendo o código de um [record](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/records) chamado `OrdemItem`. Fizemos uso de um recurso da linguagem C# chamado [`partial`](https://learn.microsoft.com/pt-br/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods) para dividir os escopos das regras de negócio dos seus testes. O código a seguir ilustra essa implementação. 

```c#
public partial record OrderItem
{
    public string? Sku { get; init; }
    public uint Quantity { get; init; }
    public decimal Price { get; init; }
    
    public decimal GetTotalPrice() => Quantity * Price;
    
    public static implicit operator OrderItem((uint quantity, decimal price) tuple) => new()
    {
        Sku = Guid.NewGuid().ToString(),
        Quantity = tuple.quantity,
        Price = tuple.price
    };
}
```

O código a seguir, ilustrar o conteúdo do arquivo `OrderItem.Test.cs`, o qual implementa as especificações do framework de testes _xUnit_, para testar o record OrderItem.

```c#
public partial record OrderItem
{
    [Theory]
    [InlineData(01, 5, 005)]
    [InlineData(05, 5, 025)]
    [InlineData(50, 5, 250)]
    [InlineData(00, 5, 000)]
    internal void get_total_price_method_should_calculate_correctly(uint quantity, decimal price, decimal expected)
    {
        OrderItem item = (quantity, price);     
        item.GetTotalPrice().Should().Be(expected);
    }
}
```

### 4. Ajustando a compilação

A fim de evitar que a versão final do software leve arquivos e pacotes desnecessários para o ambiente de produção, fizemos as seguintes intervenções no projeto.

a) adicionamos a tag a seguir, para indicar ao compilador que em versões de release remova os arquivos que estão na convenção `**\*.Tests.cs`. Isso irá remover da solução todos os arquivos de teste.

```xml
<ItemGroup Condition="'$(Configuration)' == 'Release'">
   <Compile Remove="**\*.Tests.cs" />
</ItemGroup>
```  

b) incluímos a condicional `'$(Configuration)' != 'Release'` na tag que envolve nossas dependências de testes, para indicar ao compilador que as utilize apenas em versões de desenvolvimento.

```xml
<ItemGroup Label="Test Dependecies" Condition="'$(Configuration)' != 'Release'">
    ...
</ItemGroup>
```
A versão final de nosso arquivo de projeto ficou da seguinte forma:

```xml
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    
    <ItemGroup Condition="'$(Configuration)' == 'Release'">
        <Compile Remove="**\*.Tests.cs" />
    </ItemGroup>

    <ItemGroup Label="Test Dependecies" Condition="'$(Configuration)' != 'Release'">
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
        <PackageReference Include="xunit" Version="2.4.2"/>
        <PackageReference Include="FluentAssertions" Version="6.12.0" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="coverlet.collector" Version="6.0.0">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
    </ItemGroup>
</Project>

```

### 5. Executando os testes

Com a implementação dos passos anteriores, somos capazes de executar nosso teste diretamente no projeto principal, através do comando `dotnet test` ou pelos recursos da IDE (no meu caso o [Rider](https://www.jetbrains.com/rider/)).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98ofpzx1hm1vh4drb6ps.png)

## Avaliações

Diante da implementação apresentada, concluímos que não existem empecilhos para um teste conviver junto ao seu código de produção em um projeto dotnet. Percebemos que a adoção do recurso `partial` possibilita um certo isolamento do teste em nível lógico, e ao mesmo tempo lhe concede um acesso a todo o escopo de visibilidade do código de produção. 

Outro aspecto que nos chama atenção é que o uso da convenção `**\*.Tests.cs` permite uma ordenação "natural" na árvore de arquivos do projeto. Isso facilita a localização do arquivo de testes e pode ser um incentivo para desenvolvedores mante-los organizados. Essa convenção facilita adaptações no processo de compilação para remover os arquivos de testes quando forem enviados para um ambiente produtivo.

Pensando em aspectos de [developer experience](https://github.blog/2023-06-08-developer-experience-what-is-it-and-why-should-you-care/), sinto um grande conforto e uma sensação de praticidade ao ver os testes tão próximos ao código de produção. Tendo a acreditar que a abordagem tem potencial de favorecer o processo de construção e manutenibilidade de software de forma concomitante aos testes. Contudo, algumas ponderações devem ser feitas.

## Considerações Importantes

Alguns aspectos devem ser considerados sobre a adoção dessa abordagem em soluções dotnet:  

- A abordagem parece ser apropriada para Testes de Unidade. Sendo assim, recomendamos que testes de integração e end-to-end (E2E) mantenham suas estruturas apartadas.

- Existe uma carga extra sobre compilador para remover os artefatos de testes do código de produção. Soluções que contenham muitos projetos podem ser impactadas em desempenho. 

- É preciso investigar o quanto a adoção da abordagem pode impactar as esteiras de pipeline em sua organização. Adaptações nos steps podem ser necessárias.

- Analisadores estáticos de código como o [SonarQube](https://en.wikipedia.org/wiki/SonarQube) podem apontar problemas para esse tipo de estrutura em soluções dotnet. O escopo desse artigo não explorou essa vertente.

- É preciso ponderar se a abordagem trará um benefício real para a equipe. O engajamento a essa abordagem deve estar associada á um ganho real para a equipe e organização.

De forma geral, a abordagem parece ser muito promissora para o uso em testes de unidade. Contudo, em muitos casos, seguir as convenções da comunidade e as melhores práticas para a linguagem pode ajudar a manter o código organizado, facilitar a manutenção e melhorar a colaboração entre desenvolvedores. Nesse sentido, é preciso balancear os ganhos e limitação na aplicação de qualquer nova técnica.

## Conclusão

O texto explora a prática de colocar arquivos de teste na mesma pasta que o código de produção, destacando os benefícios dessa abordagem, como facilidade de localização, incentivo à escrita de testes, e facilidade de refatoração. Além disso, compara essa prática, comum em linguagens como Go e Rust, com a separação tradicional de testes e código de produção em projetos C#.

Para isso, foi conduzida uma experiência em .NET usando *.Test.cs e partial classes para demonstrar a viabilidade e vantagens de manter testes próximos ao código. Apesar dos benefícios percebidos, como praticidade e organização melhorada, o texto pondera sobre desafios potenciais, como impacto no compilador e adaptações na pipeline de CI/CD. Ele conclui que, embora tecnicamente viável, a adoção dessa abordagem deve considerar as práticas da comunidade e as necessidades específicas do projeto

---

Caros leitores,

Espero que tenham encontrado insights valiosos na discussão sobre a co-localização de arquivos de teste e código de produção, uma prática que pode transformar significativamente o desenvolvimento de software.

Se você já experimentou essa metodologia em seus projetos, como foi? Quais vantagens e obstáculos você encontrou? Se ainda não tentou, consideraria implementá-la após essa leitura?

Seu feedback é crucial para enriquecer essa discussão. 

O código desse artigo está disponível [Aqui](https://github.com/yanjustino/test-and-production-code-in-the-same-directory/tree/main)