Pular para o conteúdo

Princípio FIRST

FIRST não é dogma; é um conjunto de heurísticas que, quando violadas, produzem sintomas previsíveis: suíte lenta, flaky, com testes que dependem uns dos outros e que ninguém confia. Cada letra abaixo traz uma violação e a correção correspondente.

graph LR
    F[Fast] --- I[Independent]
    I --- R[Repeatable]
    R --- S[Self-validating]
    S --- T[Timely]

Testes precisam ser rápidos porque só são úteis se você os roda o tempo todo. Uma suíte unitária que leva minutos deixa de ser rodada a cada mudança, e o feedback rápido — a razão de existir — evapora.

O que deixa testes lentos: I/O real (banco, rede, disco), Thread.sleep, @SpringBootTest carregando o contexto inteiro para um teste de regra de negócio.

Violação:

@SpringBootTest
class CalculadoraFreteTest {
@Autowired
CalculadoraFrete calculadora;
@Test
void calculaFretePorRegiao() throws InterruptedException {
Thread.sleep(2000);
assertThat(calculadora.calcular("SP", 5.0)).isEqualByComparingTo("15.00");
}
}

Correção — regra pura, sem contexto Spring, sem sleep:

class CalculadoraFreteTest {
CalculadoraFrete calculadora = new CalculadoraFrete();
@Test
void calculaFretePorRegiao() {
assertThat(calculadora.calcular("SP", 5.0)).isEqualByComparingTo("15.00");
}
}

Testes que precisam de I/O real existem, mas são de integração, não unitários, e vivem em outra camada da pirâmide (Tipos de Teste - Unit, Integration e E2E).

Cada teste deve montar seu próprio mundo e não depender da ordem de execução nem de estado deixado por outro teste. Testes acoplados por estado compartilhado quebram em cascata e são impossíveis de rodar isoladamente.

Violação — estado estático vazando entre testes:

class ContadorTest {
static Contador contador = new Contador();
@Test
void a_incrementa() {
contador.incrementar();
assertThat(contador.valor()).isEqualTo(1);
}
@Test
void b_incrementaDeNovo() {
contador.incrementar();
assertThat(contador.valor()).isEqualTo(1);
}
}

b_incrementaDeNovo só passa se a_incrementa rodar antes — e falha se a ordem mudar. Correção — instância nova por teste via @BeforeEach:

class ContadorTest {
Contador contador;
@BeforeEach
void setup() {
contador = new Contador();
}
@Test
void incrementaUmaVez() {
contador.incrementar();
assertThat(contador.valor()).isEqualTo(1);
}
}

Isolamento também significa isolar dependências: substituir colaboradores por dublês para que o teste falhe por um único motivo (ver Test Doubles - Stub, Spy, Mock e Fake).

O mesmo teste, rodado quantas vezes for, em qualquer máquina (sua, do colega, do CI), deve dar o mesmo resultado. Dependência de ambiente, fuso, locale, rede ou relógio quebra a repetibilidade.

Violação:

@Test
void saudacaoDaManha() {
var saudacao = new Saudacao();
assertThat(saudacao.gerar()).isEqualTo("Bom dia");
}

Passa de manhã, falha à tarde. Correção — injetar o Clock e controlar o tempo:

@Test
void saudacaoDaManha() {
Clock manha = Clock.fixed(Instant.parse("2026-07-09T09:00:00Z"), ZoneId.of("America/Sao_Paulo"));
var saudacao = new Saudacao(manha);
assertThat(saudacao.gerar()).isEqualTo("Bom dia");
}

Testes de integração que dependem de banco tornam-se repetíveis com Testcontainers, que sobe uma instância limpa e efêmera a cada execução — não um banco de dev compartilhado.

O teste decide sozinho se passou ou falhou, com um resultado booleano claro. Nada de inspecionar log no console ou comparar arquivos na mão.

Violação:

@Test
void calculaDesconto() {
var pedido = new Pedido(new BigDecimal("200"));
System.out.println("Desconto: " + pedido.desconto());
}

Sempre “passa”, porque nunca afirma nada; exige um humano lendo a saída. Correção — um assert explícito:

@Test
void calculaDesconto() {
var pedido = new Pedido(new BigDecimal("200"));
assertThat(pedido.desconto()).isEqualByComparingTo("20.00");
}

AssertJ ajuda aqui com mensagens de falha ricas e API fluente, tornando o veredito e a causa imediatamente legíveis.

Escreva os testes no momento certo — idealmente antes ou junto ao código de produção, não semanas depois. Testes adiados tendem a nunca ser escritos, e quando são, apenas confirmam o código existente em vez de guiar seu design.

Escrever o teste primeiro (a essência do Test-Driven Development) pressiona o código a ser testável: dependências viram colaborações injetáveis, interfaces ficam enxutas. Escrever depois frequentemente revela que a classe é difícil de testar — sinal de acoplamento que já custou caro para nascer.

O “Timely” é a ponte entre FIRST e TDD: não é só sobre a qualidade do teste, mas sobre o momento que maximiza seu efeito de design.

  • Slow suite normalizada: aceitar minutos de espera até ninguém mais rodar a suíte localmente.
  • Fixture compartilhada mutável: static ou campos reaproveitados sem reset, criando acoplamento por ordem.
  • @Test sem assert: passa sempre, valida nada (viola Self-validating).
  • Thread.sleep para sincronizar: lento e flaky; use Awaitility em cenários assíncronos.
  • Dependência de locale/timezone/rede: quebra Repeatable fora da sua máquina.
  • Testes escritos muito depois: perdem o poder de design e viram só documentação retroativa.

1. Por que “Fast” é pré-requisito para o valor de uma suíte unitária?

Resposta

Porque testes só entregam feedback rápido se forem executados com frequência. Uma suíte lenta deixa de ser rodada a cada mudança, e sem execução frequente o benefício central — pegar a regressão segundos depois de introduzi-la — desaparece.

2. Como estado estático compartilhado viola “Independent”?

Resposta

Ele faz um teste depender do resultado de outro, criando acoplamento por ordem de execução. O teste passa numa ordem e falha em outra, e não pode rodar isolado. A correção é recriar o objeto sob teste em @BeforeEach.

3. O que torna um teste não-repetível e como Testcontainers ajuda na integração?

Resposta

Dependência de tempo, locale, rede ou de um ambiente compartilhado. Testcontainers sobe um banco efêmero e limpo por execução, eliminando o estado residual de um banco de dev compartilhado e tornando o teste repetível em qualquer máquina.

4. Por que “Timely” conecta FIRST a TDD?

Resposta

Porque escrever o teste no momento certo (antes/junto ao código) faz o teste guiar o design rumo a algo testável e desacoplado. Adiar o teste o reduz a documentação retroativa e costuma expor acoplamento já custoso — o teste deixa de moldar o código.