Introdução

Um teste unitário é um código que executa um pedaço pequeno e isolado do seu código de produção e verifica se o resultado é o esperado. Sem rede, sem banco de dados, sem sistema de arquivos . “Unitário” quer dizer isso uma unidade pequena, (geralmente um método ou uma classe inteira) geralmente é testada sozinha, sem depender do mundo real lá fora.

Compare com os outros dois níveis que vão aparecer nos próximos módulos:

Tipo O que testa Velocidade Exemplo
Unitário (este módulo) Uma classe isolada, dependências trocadas por dublês Milissegundos LoginHandler com IIdentityService mockado
Integração (módulo 4) Várias peças reais juntas (API + banco de verdade) Segundos POST /auth/login contra SQL Server real

O que um teste unitário prova: que aquele pedaço de lógica, isolado, se comporta como esperado pra cada entrada que você pensou em testar.

O que ele NÃO prova: que o sistema inteiro funciona junto. Um LoginHandler pode ter 100% dos testes unitários passando e mesmo assim a aplicação quebrar em produção porque a connection string do banco está errada, por exemplo. Isso quem pega é o teste de integração (módulo 4). Os dois níveis se completam; nenhum substitui o outro. Isso é importante porque sem teste, a única forma de saber se um Handler/Validator funciona é rodando a API inteira na mão, criando usuário de teste, olhando a resposta. Isso leva minutos e ninguém faz isso toda vez que muda uma linha. Um teste unitário roda em milissegundos, você roda centenas deles a cada Ctrl+S, e cada um documenta uma regra de negócio que precisa continuar verdadeira.

A anatomia de um teste: Arrange-Act-Assert

Todo teste unitário bom segue a mesma estrutura de 3 partes, chamada AAA:

private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();

[Fact]
public async Task Handle_QuandoCredenciaisInvalidas_DeveRepassarErroSemAlterar()
{
    // Arrange: prepara o cenário — cria os dados de entrada e configura os mocks
    var command = new LoginCommand("[email protected]", "senha-errada");
    var erro = Error.Unauthorized("Auth.CredenciaisInvalidas", "E-mail ou senha inválidos.");
    _identityService
        .AutenticarAsync("[email protected]", "senha-errada", Arg.Any<CancellationToken>())
        .Returns(Result.Failure<TokenAcesso>(erro));

    // Act: executa a única coisa que você está testando
    var resultado = await _handler.Handle(command, CancellationToken.None);

    // Assert: verifica se o resultado é o esperado
    resultado.IsFailure.Should().BeTrue();
    resultado.Error.Should().Be(erro);
}

Se um teste tem mais de um bloco Act claramente separado testando coisas diferentes, geralmente é sinal de que deveria ser dois testes, não um.

Ferramentas principais para C#

xUnit

o framework que roda os testes. [Fact] = um teste com entrada fixa. [Theory] + [InlineData(...)] = o mesmo teste rodado várias vezes com entradas diferentes (ótimo pra testar várias entradas inválidas sem copiar e colar o teste):

[Theory]
[InlineData("sem-arroba")]
[InlineData("usuario@")]
public void Email_ComFormatoInvalido_DeveFalhar(string email) { /* roda 2x, uma por linha */ }

FluentAssertions

biblioteca de asserts que se lê como frase: resultado.Should().BeTrue(), resultado.Error.Type.Should().Be(ErrorType.Unauthorized). A vantagem sobre o Assert.Equal nativo do xUnit não é só estética — quando falha, a mensagem de erro mostra exatamente o que esperava vs. o que recebeu, de forma legível.

NSubstitute

biblioteca de mock (seção 3 explica o conceito a fundo). Substitute.For<IIdentityService>() cria um objeto falso que implementa a interface sem nenhuma lógica real; você programa o que ele deve devolver com .Returns(...).

Isolamento com mock: por que não usar o IIdentityService de verdade

LoginHandler depende de IIdentityService pra autenticar. A implementação real (IdentityService, em Core.Infrastructure) fala com o UserManager do ASP.NET Identity, que fala com o SQL Server. Se o teste do LoginHandler usasse essa implementação real, ele deixaria de ser unitário e viraria um teste de integração disfarçado, lento, e que quebra se o banco estiver fora do ar, mesmo que a lógica do LoginHandler esteja perfeita.

A solução: trocar IIdentityService por um mock — um objeto falso que implementa a mesma interface mas devolve exatamente o que você mandar, sem tocar em nada de verdade.

private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();

// "Quando chamarem AutenticarAsync com qualquer email/senha, devolva isso:"
_identityService
    .AutenticarAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
    .Returns(Result.Success(new TokenAcesso("token-fake", DateTime.UtcNow)));

Regra prática: se a classe que você está testando recebe algo pelo construtor (como LoginHandler recebe identityService e validator), isso quase sempre vira um mock no teste unitário. Se ela não recebe nada de fora (como LoginValidator, que só olha os próprios dados), não precisa de mock nenhum

Como descobrir quais testes escrever

Essa é uma pergunta comum quando os desenvolvedores estão implementando os primeiros testes da vida. Saber usar xUnit é mecânico, saber o que testar é a habilidade de verdade. Roteiro de 4 passos, aplicável a qualquer método:

Passo 1 — Caminho feliz. Qual é o uso normal, com entrada válida? Sempre o primeiro teste, e o mais óbvio.

Passo 2 — Trace cada branch do método. Abra o código e marque cada if, return antecipado, switch/pattern match. Cada branch é no mínimo um teste. Olhe o LoginHandler:

public async Task<Result<LoginResponse>> Handle(LoginCommand command, CancellationToken ct)
{
    var validacao = await validator.ValidateAsync(command, ct);
    if (!validacao.IsValid) return validacao.ToValidationError();        // branch 1

    var resultado = await identityService.AutenticarAsync(email, command.Senha, ct);
    if (resultado.IsFailure) return resultado.Error;                     // branch 2

    return new LoginResponse(resultado.Value.Token, resultado.Value.ExpiraEm); // branch 3 (caminho feliz)
}

São 3 return diferentes → no mínimo 3 testes, um pra cada um.

Passo 3 — Valores de fronteira. Pra cada entrada, pense no limite exato. A senha precisa ter 8 caracteres? Teste com 7 (deve falhar), 8 (deve passar) e talvez 9 — é ali, exatamente na fronteira, que erro de < vs <= se esconde.

Passo 4 — Cada jeito que uma dependência externa pode falhar. Pra cada mock, pergunte: de quantas formas diferentes isso pode falhar, e o código trata cada uma igual ou diferente? IIdentityService.AutenticarAsync pode devolver sucesso ou um Error com ErrorType.Unauthorized — só esses dois, então só precisa de 2 testes pra essa dependência. Já RegistrarAsync pode falhar com Conflict (e-mail duplicado) OU Validation (senha fraca) — duas formas distintas, dois testes distintos.

Nomenclatura: o nome do teste é documentação

Se o nome sozinho já explica o cenário e o resultado esperado, qualquer pessoa olhando a lista de testes entende o comportamento da classe sem abrir o código de produção — o arquivo de teste vira documentação viva.