O que exatamente é uma unidade?

Unidade não precisa significar um único método nem uma classe isolada por mock. Em muitos testes, a unidade é um comportamento observável, como calcular um desconto ou decidir se um pedido pode ser confirmado. Dependências lentas ou não determinísticas — rede, relógio, banco — costumam ficar fora desse teste para que ele seja rápido e reproduzível.

Uma classe que implementa regra de domínio pode ser testada sem doubles; um caso de uso que depende de persistência pode receber uma implementação falsa pequena. O limite é uma escolha de desenho. Se o teste exigir muita infraestrutura, pergunte se ele cobre uma integração e deveria ser classificado como tal.

Organize o teste em cenário, ação e resultado

Use nomes que descrevam condição e resultado, entradas explícitas e uma asserção que expresse o contrato. Arrange-Act-Assert é uma convenção útil, não obrigação: prepare os dados, execute o comportamento e verifique um resultado relevante.

public sealed class DescontoTests
{
    [Theory]
    [InlineData(0, 100)]
    [InlineData(10, 90)]
    [InlineData(100, 0)]
    public void Aplica_percentual_valido(
        decimal percentual, decimal totalEsperado)
    {
        var pedido = new Pedido(total: 100m);

        pedido.AplicarDesconto(percentual);

        Assert.Equal(totalEsperado, pedido.Total);
    }
}

Exemplo ilustrativo em xUnit: tipo, construtor e regras dependem do modelo real. A validação de limites, como desconto negativo ou acima de 100%, merece cenários próprios conforme o contrato do domínio. Não use valores de ponto flutuante sem considerar precisão monetária.

Use test doubles com intenção

Um stub retorna uma resposta preparada; um fake implementa uma dependência de forma simplificada e funcional, como um repositório em memória; um mock verifica interações. Escolha o menor recurso que torne o teste claro. Mockar cada método interno pode fazer o teste passar mesmo quando o comportamento final está errado e torna refatorações barulhentas.

Prefira verificar efeitos observáveis. Se a regra exige publicar um evento, confirme que o evento correto foi produzido; não verifique chamadas incidentais sem significado para o contrato. Para banco, HTTP e mensageria reais, escreva testes de integração delimitados em vez de tentar simular cada detalhe.

Evite testes não determinísticos

Um teste deve passar ou falhar pelas condições que controla, não por horário, ordem de execução, rede, aleatoriedade ou estado deixado por outro teste. Injete abstrações para relógio ou geradores de identificadores quando essas variações importam. Use dados independentes e limpe recursos com o ciclo de vida adequado.

Paralelismo e fixtures compartilhadas podem economizar tempo, mas estado mutável global cria interferência. Não desative paralelismo para esconder dependências; isole o recurso ou coordene o acesso explicitamente.

Cobertura e quantidade não são qualidade

Cobertura de linhas informa quais instruções foram executadas, não se os resultados foram verificados. Uma linha pode ser coberta por um teste sem nenhuma asserção útil. Foque em riscos, limites, decisões de negócio e cenários de falha. Mutation testing pode indicar se pequenas alterações no código seriam percebidas, mas seus resultados também exigem interpretação.

Escreva o teste antes da implementação se estiver praticando TDD; use exemplos de domínio compartilhados com BDD quando a regra precisar ser entendida por pessoas de diferentes funções.

Execução e próximos níveis

Uma suíte .NET comum roda com dotnet test. Mantenha testes unitários rápidos na verificação frequente, e execute testes mais caros conforme risco e pipeline. Testes unitários não validam por si só o roteamento HTTP, SQL específico do provedor nem compatibilidade entre serviços.

Para integração real, veja ASP.NET Core e containers; para testes funcionais no navegador, consulte testes funcionais e Cypress.

Documentação e leituras relacionadas