O Princípio FIRST em Testes de Software
Introdução Robert C. Martin, mais conhecido como "Uncle Bob", é um dos nomes mais respeitados na área de desenvolvimento de software e autor de obras influentes como "Clean Code" e "Clean Architecture". Entre suas muitas contribuições para boas práticas de programação, Uncle Bob popularizou o acrônimo FIRST, que define cinco características essenciais para testes de software de qualidade. O princípio FIRST estabelece que testes de unidade devem ser: Fast (Rápidos) Independent (Independentes) Repeatable (Repetíveis) Self-validating (Auto-validáveis) Timely (Oportunos) / Thorough (Completos) Nota: Existem duas interpretações comuns para o "T" em FIRST: "Timely" (Oportunos) e "Thorough" (Completos). Ambas são válidas e complementares no contexto de boas práticas de teste. Vamos explorar cada um desses princípios em detalhes e ver exemplos práticos em Java de como aplicá-los. Fast (Rápidos) O que significa? Testes devem ser rápidos para executar. Idealmente, centenas ou milhares de testes devem rodar em poucos segundos. Isso permite que os desenvolvedores executem os testes com frequência durante o desenvolvimento, o que é essencial para o ciclo de feedback rápido. Por que é importante? Testes lentos desencorajam os desenvolvedores de executá-los frequentemente. Se os testes demorarem muito tempo para executar, os desenvolvedores podem ser tentados a pular a etapa de testes, o que pode levar a bugs não detectados. Exemplo em Java: // Exemplo de teste rápido public class CalculadoraTest { @Test public void testSoma() { Calculadora calc = new Calculadora(); assertEquals(5, calc.soma(2, 3)); } } Anti-exemplo: // Teste lento - deve ser evitado @Test public void testOperacaoComBancoDeDados() { // Configuração de conexão com banco real Database db = new Database("jdbc:mysql://localhost:3306/banco"); // Operações de leitura/escrita no banco // ... // Teste que depende de operações I/O lentas } Como corrigir: Use mocks e stubs para simular dependências externas Evite operações de I/O nos testes unitários (banco de dados, sistema de arquivos, rede) Deixe testes mais complexos/lentos para testes de integração Independent (Independentes) O que significa? Cada teste deve ser independente dos outros. Um teste não deve depender do estado deixado por outro teste, nem a ordem de execução deve importar. Por que é importante? Testes interdependentes são frágeis e difíceis de depurar. Se o teste A falhar, e o teste B depender dele, B também falhará, mesmo que a funcionalidade que B está testando esteja funcionando corretamente. Exemplo em Java: public class ContaBancariaTest { private ContaBancaria conta; @Before public void setUp() { // Cada teste começa com uma conta nova com 100 reais conta = new ContaBancaria(100.0); } @Test public void testSaque() { assertTrue(conta.sacar(50.0)); assertEquals(50.0, conta.getSaldo(), 0.001); } @Test public void testSaqueMaiorQueSaldo() { assertFalse(conta.sacar(150.0)); assertEquals(100.0, conta.getSaldo(), 0.001); } } Anti-exemplo: public class ContaBancariaTestRuim { // Estado compartilhado entre testes - má prática private static ContaBancaria contaCompartilhada = new ContaBancaria(100.0); @Test public void test1_Saque() { assertTrue(contaCompartilhada.sacar(50.0)); assertEquals(50.0, contaCompartilhada.getSaldo(), 0.001); } @Test public void test2_SaqueMaiorQueSaldo() { // Este teste vai falhar porque depende do estado deixado pelo teste anterior // O saldo agora é 50, não 100 assertFalse(contaCompartilhada.sacar(150.0)); assertEquals(50.0, contaCompartilhada.getSaldo(), 0.001); } } Como corrigir: Use métodos @Before (JUnit 4) ou @BeforeEach (JUnit 5) para configurar um ambiente limpo antes de cada teste Evite variáveis estáticas compartilhadas entre testes Não nomeie testes com prefixos numéricos para forçar uma ordem de execução Repeatable (Repetíveis) O que significa? Os testes devem produzir o mesmo resultado toda vez que forem executados, independentemente do ambiente ou timing. Eles devem funcionar em qualquer ambiente, seja no servidor de CI, no laptop de desenvolvimento ou na máquina de um colega. Por que é importante? Testes não repetíveis são imprevisíveis e podem levar a falsos positivos ou falsos negativos. Isso diminui a confiança nos testes e pode levar a bugs difíceis de rastrear. Exemplo em Java: public class GeradorDeRelatoriosTest { @Test public void testGeracaoRelatorio() { // Usando uma data fixa em vez da data atual LocalDate dataFixa = LocalDate.of(2023, 4, 10); Ger

Introdução
Robert C. Martin, mais conhecido como "Uncle Bob", é um dos nomes mais respeitados na área de desenvolvimento de software e autor de obras influentes como "Clean Code" e "Clean Architecture". Entre suas muitas contribuições para boas práticas de programação, Uncle Bob popularizou o acrônimo FIRST, que define cinco características essenciais para testes de software de qualidade.
O princípio FIRST estabelece que testes de unidade devem ser:
- Fast (Rápidos)
- Independent (Independentes)
- Repeatable (Repetíveis)
- Self-validating (Auto-validáveis)
- Timely (Oportunos) / Thorough (Completos)
Nota: Existem duas interpretações comuns para o "T" em FIRST: "Timely" (Oportunos) e "Thorough" (Completos). Ambas são válidas e complementares no contexto de boas práticas de teste.
Vamos explorar cada um desses princípios em detalhes e ver exemplos práticos em Java de como aplicá-los.
Fast (Rápidos)
O que significa?
Testes devem ser rápidos para executar. Idealmente, centenas ou milhares de testes devem rodar em poucos segundos. Isso permite que os desenvolvedores executem os testes com frequência durante o desenvolvimento, o que é essencial para o ciclo de feedback rápido.
Por que é importante?
Testes lentos desencorajam os desenvolvedores de executá-los frequentemente. Se os testes demorarem muito tempo para executar, os desenvolvedores podem ser tentados a pular a etapa de testes, o que pode levar a bugs não detectados.
Exemplo em Java:
// Exemplo de teste rápido
public class CalculadoraTest {
@Test
public void testSoma() {
Calculadora calc = new Calculadora();
assertEquals(5, calc.soma(2, 3));
}
}
Anti-exemplo:
// Teste lento - deve ser evitado
@Test
public void testOperacaoComBancoDeDados() {
// Configuração de conexão com banco real
Database db = new Database("jdbc:mysql://localhost:3306/banco");
// Operações de leitura/escrita no banco
// ...
// Teste que depende de operações I/O lentas
}
Como corrigir:
- Use mocks e stubs para simular dependências externas
- Evite operações de I/O nos testes unitários (banco de dados, sistema de arquivos, rede)
- Deixe testes mais complexos/lentos para testes de integração
Independent (Independentes)
O que significa?
Cada teste deve ser independente dos outros. Um teste não deve depender do estado deixado por outro teste, nem a ordem de execução deve importar.
Por que é importante?
Testes interdependentes são frágeis e difíceis de depurar. Se o teste A falhar, e o teste B depender dele, B também falhará, mesmo que a funcionalidade que B está testando esteja funcionando corretamente.
Exemplo em Java:
public class ContaBancariaTest {
private ContaBancaria conta;
@Before
public void setUp() {
// Cada teste começa com uma conta nova com 100 reais
conta = new ContaBancaria(100.0);
}
@Test
public void testSaque() {
assertTrue(conta.sacar(50.0));
assertEquals(50.0, conta.getSaldo(), 0.001);
}
@Test
public void testSaqueMaiorQueSaldo() {
assertFalse(conta.sacar(150.0));
assertEquals(100.0, conta.getSaldo(), 0.001);
}
}
Anti-exemplo:
public class ContaBancariaTestRuim {
// Estado compartilhado entre testes - má prática
private static ContaBancaria contaCompartilhada = new ContaBancaria(100.0);
@Test
public void test1_Saque() {
assertTrue(contaCompartilhada.sacar(50.0));
assertEquals(50.0, contaCompartilhada.getSaldo(), 0.001);
}
@Test
public void test2_SaqueMaiorQueSaldo() {
// Este teste vai falhar porque depende do estado deixado pelo teste anterior
// O saldo agora é 50, não 100
assertFalse(contaCompartilhada.sacar(150.0));
assertEquals(50.0, contaCompartilhada.getSaldo(), 0.001);
}
}
Como corrigir:
- Use métodos
@Before
(JUnit 4) ou@BeforeEach
(JUnit 5) para configurar um ambiente limpo antes de cada teste - Evite variáveis estáticas compartilhadas entre testes
- Não nomeie testes com prefixos numéricos para forçar uma ordem de execução
Repeatable (Repetíveis)
O que significa?
Os testes devem produzir o mesmo resultado toda vez que forem executados, independentemente do ambiente ou timing. Eles devem funcionar em qualquer ambiente, seja no servidor de CI, no laptop de desenvolvimento ou na máquina de um colega.
Por que é importante?
Testes não repetíveis são imprevisíveis e podem levar a falsos positivos ou falsos negativos. Isso diminui a confiança nos testes e pode levar a bugs difíceis de rastrear.
Exemplo em Java:
public class GeradorDeRelatoriosTest {
@Test
public void testGeracaoRelatorio() {
// Usando uma data fixa em vez da data atual
LocalDate dataFixa = LocalDate.of(2023, 4, 10);
GeradorDeRelatorios gerador = new GeradorDeRelatorios();
String relatorio = gerador.gerarRelatorio(dataFixa);
assertTrue(relatorio.contains("Relatório de 10/04/2023"));
}
}
Anti-exemplo:
public class GeradorDeRelatoriosTestRuim {
@Test
public void testGeracaoRelatorio() {
// Usando a data atual - o teste pode falhar no futuro
GeradorDeRelatorios gerador = new GeradorDeRelatorios();
String relatorio = gerador.gerarRelatorio(LocalDate.now());
// Este assert pode falhar dependendo do dia em que o teste é executado
assertTrue(relatorio.contains("Relatório de " + LocalDate.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))));
}
}
Como corrigir:
- Evite depender de dados variáveis como data/hora atual, valores aleatórios
- Use injeção de dependência para controlar fontes de dados externos
- Simule componentes externos que podem variar (APIs, banco de dados)
- Use dados de teste fixos e predeterminados
Self-validating (Auto-validáveis)
O que significa?
Os testes devem determinar automaticamente se passaram ou falharam, sem necessidade de interpretação humana dos resultados. Eles devem produzir uma saída binária: passou ou falhou.
Por que é importante?
Testes que requerem interpretação manual são propensos a erros e consomem tempo. A automação completa permite que os testes sejam executados como parte de um processo de integração contínua.
Exemplo em Java:
public class ProcessadorDePagamentoTest {
@Test
public void testPagamentoComSucesso() {
Cartao cartao = new Cartao("1234-5678-9012-3456", "12/2025", "123");
ProcessadorDePagamento processador = new ProcessadorDePagamento();
ResultadoPagamento resultado = processador.processar(cartao, 100.0);
assertTrue(resultado.sucesso());
assertEquals("Pagamento aprovado", resultado.mensagem());
}
}
Anti-exemplo:
public class ProcessadorDePagamentoTestRuim {
@Test
public void testPagamentoComSucesso() {
Cartao cartao = new Cartao("1234-5678-9012-3456", "12/2025", "123");
ProcessadorDePagamento processador = new ProcessadorDePagamento();
ResultadoPagamento resultado = processador.processar(cartao, 100.0);
// Imprime resultado para interpretação manual - má prática
System.out.println("Resultado do pagamento: " + resultado.mensagem());
// Não há assertions para validar automaticamente
}
}
Como corrigir:
- Use sempre assertions para validar resultados
- Não dependa de inspeção manual de logs ou output do console
- Automatize completamente a verificação dos resultados esperados
Timely (Oportunos)
O que significa?
Os testes devem ser escritos no momento certo - idealmente antes do código de produção (TDD - Test-Driven Development) ou, pelo menos, ao mesmo tempo em que o código está sendo escrito.
Por que é importante?
Escrever testes após o código já estar pronto pode tornar o código mais difícil de testar, levando a design menos modular. Além disso, há o risco de o desenvolvedor focar apenas em caminhos felizes, ignorando casos de borda.
Exemplo em Java (TDD):
// 1. Primeiro, escrevemos o teste
@Test
public void testValidacaoEmail() {
ValidadorEmail validador = new ValidadorEmail();
assertTrue(validador.ehValido("usuario@exemplo.com"));
assertFalse(validador.ehValido("usuario@"));
assertFalse(validador.ehValido("usuario"));
assertFalse(validador.ehValido("@exemplo.com"));
}
// 2. Depois, implementamos o código para passar no teste
public class ValidadorEmail {
public boolean ehValido(String email) {
if (email == null || email.isEmpty()) {
return false;
}
// Verifica se contém @ e se não está no início ou fim
int posicaoArroba = email.indexOf('@');
if (posicaoArroba <= 0 || posicaoArroba == email.length() - 1) {
return false;
}
// Verifica se há texto após o @ e se contém pelo menos um ponto
String dominio = email.substring(posicaoArroba + 1);
return dominio.contains(".") && !dominio.startsWith(".");
}
}
Anti-exemplo:
// Implementação sem testes prévios
public class ValidadorEmailSemTestes {
public boolean ehValido(String email) {
return email != null && email.contains("@");
}
}
// Testes escritos tardiamente, após a implementação estar "completa"
@Test
public void testValidacaoEmailTardio() {
ValidadorEmailSemTestes validador = new ValidadorEmailSemTestes();
assertTrue(validador.ehValido("usuario@exemplo.com"));
// Ops, descobrimos que a implementação é muito simplista
// e aceita emails claramente inválidos
assertTrue(validador.ehValido("usuario@")); // Este deveria falhar!
}
Como corrigir:
- Adote o TDD ou, pelo menos, escreva testes em paralelo com o código
- Use testes para guiar o design do código
- Considere testes como parte da implementação, não como uma tarefa adicional
Thorough (Completos)
O que significa?
Os testes devem ser completos e minuciosos, cobrindo não apenas os caminhos felizes, mas também os casos de borda, condições de erro e situações excepcionais que o código pode enfrentar.
Por que é importante?
Testes que cobrem apenas o cenário ideal podem deixar passar bugs em casos menos comuns, mas igualmente importantes. Uma cobertura abrangente garante que o código seja robusto em todas as situações possíveis.
Exemplo em Java:
public class ValidadorSenhaTest {
private ValidadorSenha validador;
@Before
public void setUp() {
validador = new ValidadorSenha();
}
// Casos felizes
@Test
public void testSenhaValida() {
assertTrue(validador.validar("Abcd1234!"));
assertTrue(validador.validar("P@ssw0rd123"));
}
// Casos de borda
@Test
public void testTamanhoMinimo() {
assertFalse(validador.validar("Ab1!")); // Muito curta
assertTrue(validador.validar("Abcd1234!")); // Mínimo exato
}
@Test
public void testTamanhoMaximo() {
assertTrue(validador.validar("Abcd1234!Abcd1234!")); // Ok
assertFalse(validador.validar("Abcd1234!Abcd1234!Abcd1234!Abcd1234!")); // Muito longa
}
// Casos de erro
@Test
public void testSenhaComCaracteresEspeciais() {
assertTrue(validador.validar("Abcd1234!"));
assertFalse(validador.validar("Abcd1234")); // Sem caractere especial
}
@Test
public void testSenhaComNumeros() {
assertTrue(validador.validar("Abcd1234!"));
assertFalse(validador.validar("Abcd!")); // Sem números
}
@Test
public void testSenhaComLetrasMaiusculas() {
assertTrue(validador.validar("Abcd1234!"));
assertFalse(validador.validar("abcd1234!")); // Sem maiúsculas
}
@Test
public void testSenhaComLetrasMinusculas() {
assertTrue(validador.validar("Abcd1234!"));
assertFalse(validador.validar("ABCD1234!")); // Sem minúsculas
}
// Casos nulos ou inválidos
@Test
public void testSenhaNula() {
assertFalse(validador.validar(null));
}
@Test
public void testSenhaVazia() {
assertFalse(validador.validar(""));
}
@Test
public void testSenhaComEspacos() {
assertFalse(validador.validar("Abcd 1234!"));
}
}
Anti-exemplo:
public class ValidadorSenhaTestInsuficiente {
@Test
public void testSenhaValida() {
ValidadorSenha validador = new ValidadorSenha();
// Testa apenas o caminho feliz, ignorando todos os casos de borda e erros
assertTrue(validador.validar("Abcd1234!"));
}
}
Como aplicar:
- Identifique todos os requisitos e restrições da funcionalidade
- Teste os casos felizes, casos de borda e condições de erro
- Use técnicas como análise de valores limite e particionamento de equivalência
- Considere usar ferramentas de cobertura de código para identificar caminhos não testados
- Inclua testes para entradas nulas, vazias ou inválidas
- Verifique comportamentos com valores mínimos, máximos e fora dos limites
Aplicação Prática: Exemplo Completo
Vamos criar um exemplo completo aplicando todos os princípios FIRST (incluindo ambas as interpretações do "T") para uma classe CalculadoraDeJuros
:
// Classe de produção
public class CalculadoraDeJuros {
private final RepositorioTaxas repositorio;
public CalculadoraDeJuros(RepositorioTaxas repositorio) {
this.repositorio = repositorio;
}
public double calcularJurosCompostos(double principal, int anos, String tipoDeConta) {
double taxa = repositorio.obterTaxaAnual(tipoDeConta);
if (taxa <= 0) {
throw new IllegalArgumentException("Taxa de juros deve ser positiva");
}
return principal * Math.pow(1 + taxa, anos);
}
}
// Interface do repositório
public interface RepositorioTaxas {
double obterTaxaAnual(String tipoDeConta);
}
Agora, vamos criar testes que seguem o princípio FIRST:
// Importações para JUnit 5
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class CalculadoraDeJurosTest {
// Mock do repositório para tornar o teste Fast e Repeatable
private RepositorioTaxas mockRepositorio;
private CalculadoraDeJuros calculadora;
@BeforeEach // JUnit 5 usa @BeforeEach em vez de @Before do JUnit 4
void setUp() {
// Setup para cada teste - torna os testes Independent
mockRepositorio = mock(RepositorioTaxas.class);
calculadora = new CalculadoraDeJuros(mockRepositorio);
}
@Test
@DisplayName("Deve calcular juros compostos corretamente") // Anotação descritiva do JUnit 5
void testCalculoJurosCompostos() {
// Arrange
when(mockRepositorio.obterTaxaAnual("poupanca")).thenReturn(0.05); // 5%
// Act
double resultado = calculadora.calcularJurosCompostos(1000.0, 3, "poupanca");
// Assert - torna o teste Self-validating
assertEquals(1157.625, resultado, 0.001);
}
@Test
@DisplayName("Deve calcular juros para diferentes tipos de contas")
void testCalculoJurosCompostosParaDiferentesTiposDeContas() {
// Arrange
when(mockRepositorio.obterTaxaAnual("poupanca")).thenReturn(0.05); // 5%
when(mockRepositorio.obterTaxaAnual("investimento")).thenReturn(0.08); // 8%
// Act & Assert
assertEquals(1157.625, calculadora.calcularJurosCompostos(1000.0, 3, "poupanca"), 0.001);
assertEquals(1259.712, calculadora.calcularJurosCompostos(1000.0, 3, "investimento"), 0.001);
}
@Test
@DisplayName("Deve lançar exceção para taxa negativa")
void testExcecaoParaTaxaNegativa() {
// Arrange
when(mockRepositorio.obterTaxaAnual("invalida")).thenReturn(-0.01);
// Act & Assert - JUnit 5 usa assertThrows em vez de expected
assertThrows(IllegalArgumentException.class, () -> {
calculadora.calcularJurosCompostos(1000.0, 3, "invalida");
});
}
}
Este exemplo ilustra os princípios FIRST:
- Fast: Os testes não dependem de recursos externos lentos, usando mocks para o repositório.
- Independent: Cada teste tem seu próprio setup e não depende de estado compartilhado.
- Repeatable: Os resultados dos testes serão os mesmos independentemente de onde ou quando forem executados, pois usamos dados mock fixos.
- Self-validating: Cada teste contém assertions claras que determinam automaticamente o sucesso ou falha.
- T (Timely/Thorough): → Timely: Os testes foram escritos para guiar o design da classe, garantindo que ela seja testável desde o início. → Thorough: Os testes cobrem tanto o comportamento normal (cálculo de juros correto) quanto condições de erro (taxas negativas).
Conclusão
O princípio FIRST fornece um guia valioso para escrever testes de unidade eficazes. Adotar essas práticas leva a:
- Testes mais confiáveis e robustos
- Ciclos de feedback mais rápidos durante o desenvolvimento
- Código mais limpo e com melhor design
- Maior confiança nas mudanças e refatorações
- Documentação viva do comportamento esperado do sistema
Ao seguir esses princípios consistentemente, as equipes de desenvolvimento podem criar uma suíte de testes que realmente agrega valor ao projeto, em vez de se tornar um fardo de manutenção. Lembre-se de que testes bem escritos são um investimento que paga dividendos ao longo da vida útil do software.