Testes automatizados são uma das práticas mais importantes para manter a qualidade de aplicações de software.
Entretanto, existe uma dúvida bastante comum entre desenvolvedores .NET: devemos investir em testes unitários, testes de integração ou implementar os dois?
Em muitos projetos, encontramos situações extremas.
Algumas equipes escrevem exclusivamente testes unitários, utilizando mocks para praticamente todas as dependências. Outras preferem testar tudo por meio de testes de integração, executando a aplicação com banco de dados e infraestrutura real.
Ambas as abordagens podem apresentar limitações.
Testes unitários oferecem velocidade e isolamento. Testes de integração verificam se diferentes componentes realmente funcionam juntos.
Neste artigo, vamos entender as diferenças entre essas estratégias, conhecer exemplos utilizando C# e xUnit e descobrir como estruturar uma suíte de testes eficiente para projetos .NET.
Testes automatizados são verificações executadas por código para confirmar se determinada funcionalidade apresenta o comportamento esperado.
Em vez de depender exclusivamente de verificações manuais, criamos testes que podem ser executados repetidamente.
Isso permite identificar regressões, validar alterações e aumentar a confiança durante o desenvolvimento.
No ecossistema .NET, existem diferentes frameworks para trabalhar com testes:
Todos permitem implementar testes unitários e de integração.
Neste artigo, utilizaremos xUnit nos exemplos.
O teste unitário tem como objetivo verificar o comportamento de uma unidade específica da aplicação, normalmente um método ou uma classe.
A principal característica é o isolamento.
Imagine uma classe responsável por calcular descontos de pedidos.
public class DiscountCalculator
{
public decimal Calculate(decimal amount)
{
if (amount >= 1000)
return amount * 0.10m;
return amount * 0.05m;
}
}
Podemos criar um teste unitário para verificar seu comportamento:
public class DiscountCalculatorTests
{
[Fact]
public void Calculate_WhenAmountIsGreaterThan1000_Returns10Percent()
{
var calculator = new DiscountCalculator();
var result = calculator.Calculate(1500);
Assert.Equal(150m, result);
}
}
O teste verifica se um pedido de 1.500 recebe corretamente um desconto de 10%.
Observe que não precisamos de banco de dados, API externa ou infraestrutura adicional.
O teste é rápido, simples e independente.
São especialmente indicados para:
Quanto mais complexas forem as regras de negócio, maior tende a ser o benefício de possuir testes unitários bem estruturados.
Em aplicações reais, nossas classes normalmente dependem de outros componentes.
Imagine um serviço responsável por criar pedidos e salvar as informações em um repositório.
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
O serviço:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task CreateAsync(Order order)
{
if (order.Amount <= 0)
throw new ArgumentException("Invalid amount");
await _repository.SaveAsync(order);
}
}
Para testar essa classe sem depender de um banco de dados, podemos utilizar um mock.
Com a biblioteca Moq:
[Fact]
public async Task CreateAsync_WithValidOrder_SavesOrder()
{
var repository = new Mock<IOrderRepository>();
var service = new OrderService(repository.Object);
var order = new Order
{
Amount = 500
};
await service.CreateAsync(order);
repository.Verify(
x => x.SaveAsync(order),
Times.Once);
}
Nesse cenário, estamos verificando se o serviço chama corretamente o repositório.
Não estamos testando se o banco de dados realmente salva o pedido.
Essa distinção é fundamental.
Um teste unitário pode confirmar que determinada interação aconteceu, mas não garante que toda a infraestrutura esteja funcionando corretamente.
Enquanto os testes unitários verificam componentes isoladamente, os testes de integração avaliam se diferentes partes da aplicação funcionam corretamente quando conectadas.
Imagine uma API ASP.NET Core que recebe uma requisição, valida os dados, executa uma regra de negócio e salva informações no SQL Server.
Um teste de integração pode verificar esse fluxo.
Nesse cenário, podemos validar:
O ASP.NET Core disponibiliza a classe WebApplicationFactory, que facilita a criação de um ambiente de testes para a aplicação.
Para utilizá-la, normalmente precisamos do pacote:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
Um exemplo simplificado:
public class OrdersApiTests :
IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrdersApiTests(
WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetOrders_ReturnsSuccessStatusCode()
{
var response = await _client.GetAsync("/api/orders");
response.EnsureSuccessStatusCode();
}
}
Esse exemplo utiliza o servidor de testes do ASP.NET Core para executar uma requisição HTTP contra a aplicação.
Para testar cenários mais próximos da produção, podemos substituir configurações e dependências, utilizar um banco de dados de testes e preparar dados específicos.
As duas abordagens possuem objetivos diferentes.
| Característica | Testes unitários | Testes de integração |
|---|---|---|
| Objetivo | Validar unidades isoladas | Validar componentes integrados |
| Velocidade | Geralmente muito rápidos | Geralmente mais lentos |
| Banco de dados | Normalmente não utiliza | Pode utilizar |
| Dependências | Frequentemente simuladas | Preferencialmente reais no escopo testado |
| Complexidade | Menor | Maior |
| Diagnóstico | Mais localizado | Pode envolver vários componentes |
| Uso ideal | Regras de negócio | Infraestrutura e fluxos integrados |
Um detalhe importante: testes de integração não precisam obrigatoriamente utilizar todos os serviços reais da produção.
É perfeitamente possível substituir serviços externos por implementações controladas, desde que o objetivo do teste seja claramente definido.
Sim. Para a maioria das aplicações profissionais, uma combinação equilibrada costuma ser a melhor estratégia.
Imagine uma aplicação de processamento de pagamentos.
Podemos organizar os testes da seguinte maneira:
Dessa maneira, evitamos utilizar testes de integração para verificar comportamentos que poderiam ser validados de maneira muito mais simples por testes unitários.
A documentação oficial do ASP.NET Core também recomenda limitar os testes de integração aos cenários de infraestrutura mais importantes, utilizando testes unitários quando ambos poderiam validar o mesmo comportamento.
Uma boa organização facilita a manutenção e permite executar diferentes categorias de testes separadamente.
Uma estrutura possível:
MyApplication
│
├── src
│ ├── MyApplication.Api
│ ├── MyApplication.Application
│ ├── MyApplication.Domain
│ └── MyApplication.Infrastructure
│
└── tests
├── MyApplication.UnitTests
└── MyApplication.IntegrationTests
O projeto UnitTests concentra os testes de regras de negócio, serviços e componentes isolados.
O projeto IntegrationTests concentra os testes que dependem da integração entre diferentes partes da solução.
Essa separação também permite executar apenas os testes unitários durante ciclos rápidos de desenvolvimento.
Testes automatizados precisam fazer parte do fluxo de desenvolvimento.
Uma das maneiras mais simples de executar os testes é utilizar o próprio CLI do .NET:
dotnet test
Também podemos executar um projeto específico:
dotnet test tests/MyApplication.UnitTests
Em ambientes de CI/CD, podemos incluir a execução dos testes antes de permitir que alterações sejam integradas à branch principal.
Um pipeline básico pode seguir esta sequência:
Isso ajuda a identificar problemas antes que o código chegue aos ambientes de homologação ou produção.
Existem algumas práticas que podem comprometer a qualidade da suíte de testes.
Quando praticamente tudo é simulado, podemos acabar testando apenas as interações entre mocks, sem validar comportamentos realmente importantes.
Um teste deve verificar o resultado esperado de uma funcionalidade, e não depender desnecessariamente de detalhes de implementação.
Cada teste deve ser suficientemente independente para ser executado isoladamente.
Testar apenas o caminho de sucesso deixa muitas possibilidades de falha sem validação.
Uma aplicação pode possuir centenas de testes unitários e ainda apresentar problemas na comunicação com o banco de dados ou na configuração do pipeline HTTP.
Testes unitários e testes de integração não são estratégias concorrentes.
Eles resolvem problemas diferentes e se complementam.
Os testes unitários oferecem velocidade, isolamento e facilidade de diagnóstico. Os testes de integração aumentam a confiança de que os componentes realmente funcionam juntos.
Para aplicações .NET profissionais, a recomendação é começar pelas regras de negócio, construir uma boa cobertura unitária e adicionar testes de integração para validar os principais fluxos e pontos de infraestrutura.
O objetivo não é possuir a maior quantidade possível de testes, mas construir uma suíte que realmente ajude a prevenir problemas e permita evoluir a aplicação com segurança.