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.
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.
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 */ }
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.
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(...).
IIdentityService de verdadeLoginHandler 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
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.
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.