Suíte verde, deploy tranquilo, bug em produção na sexta.
Você tem 140 testes. Todos passam. A pipeline fica verdinha, o badge de cobertura mostra 78% e dá aquele conforto de "tá coberto".
Aí um usuário reporta que o pedido foi criado mas o e-mail de confirmação nunca chegou. Você vai olhar: o teste do store existe, passa, e nunca reclamou de nada.
Como? Porque ele nunca olhou pro e-mail. Ele olhou pro código de status HTTP.
O teste que aprova qualquer coisa
Esse aqui você já escreveu. Eu já escrevi.
public function test_cria_pedido()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/pedidos', [
'produto_id' => 1,
'quantidade' => 2,
]);
$response->assertStatus(200); // 🎉 verde!
}
Agora um exercício desconfortável: vai no controller e comenta a linha que envia o e-mail.
O teste continua passando.
Comenta a linha que calcula o desconto. Passa.
Comenta a linha que baixa o estoque. Passa.
Esse teste não testa a criação de pedido. Ele testa que a rota existe e que o Laravel não explodiu no caminho. É o dd() dos testes: te dá uma sensação de verificação sem verificar quase nada.
A régua: se eu quebrar o código, o teste reclama?
Essa é a única pergunta que importa na hora de avaliar um teste.
Um teste existe pra falhar quando o comportamento muda. Um teste que passa independente do que o código faz não é rede de segurança — é decoração cara, que ainda por cima demora pra rodar.
O nome disso na literatura é teste sem poder de detecção. O nome no dia a dia é "mas o teste tava verde".
Testando o que a feature realmente promete
O que criar um pedido significa pro negócio? Provavelmente: grava no banco, baixa o estoque, dispara um e-mail. Então é isso que o teste afirma.
public function test_cria_pedido()
{
Mail::fake();
$user = User::factory()->create();
$produto = Produto::factory()->create(['estoque' => 10]);
$response = $this->actingAs($user)->post('/pedidos', [
'produto_id' => $produto->id,
'quantidade' => 2,
]);
$response->assertRedirect('/pedidos');
// O pedido existe mesmo, com os dados certos
$this->assertDatabaseHas('pedidos', [
'user_id' => $user->id,
'produto_id' => $produto->id,
'quantidade' => 2,
]);
// O estoque baixou
$this->assertEquals(8, $produto->fresh()->estoque);
// O e-mail saiu, pra pessoa certa
Mail::assertSent(PedidoConfirmado::class, fn ($mail) => $mail->hasTo($user->email));
}
Agora volta lá e comenta a linha do e-mail. Vermelho. Comenta o estoque. Vermelho.
Esse teste tem opinião sobre o que o sistema faz. Por isso ele serve pra alguma coisa.
Os fakes que o Laravel te dá de graça
Metade dos testes fracos que eu vejo são fracos porque a pessoa não sabia que dava pra verificar aquilo. O Laravel entrega quase tudo pronto:
Mail::fake(); Mail::assertSent(BoasVindas::class);
Queue::fake(); Queue::assertPushed(ProcessarPagamento::class);
Event::fake(); Event::assertDispatched(PedidoCriado::class);
Bus::fake(); Bus::assertChained([...]);
Storage::fake('s3'); Storage::disk('s3')->assertExists('nota.pdf');
Http::fake(); Http::assertSent(fn ($req) => $req->url() === 'https://api.exemplo.com/cobranca');
Cada linha dessas é um efeito colateral que deixa de ser invisível pro seu teste.
O caminho feliz é metade do trabalho
Outro padrão comum: existe um teste por rota, e ele sempre manda os dados perfeitos.
Só que bug quase nunca mora no caminho feliz. Mora na quantidade negativa, no estoque insuficiente, no usuário sem permissão, no produto que sumiu no meio do caminho.
public function test_nao_cria_pedido_sem_estoque()
{
$produto = Produto::factory()->create(['estoque' => 1]);
$response = $this->actingAs(User::factory()->create())
->post('/pedidos', ['produto_id' => $produto->id, 'quantidade' => 5]);
$response->assertSessionHasErrors('quantidade');
$this->assertDatabaseCount('pedidos', 0); // e não criou nada
}
Repara no assertDatabaseCount. Não basta afirmar que deu erro — tem que afirmar que nada aconteceu. Já vi mais de um caso de validação que retorna erro pro usuário e mesmo assim deixa um registro meio criado pra trás.
Pegadinha: mockar demais é testar o próprio mock
Tem o extremo oposto também, e ele engana mais gente porque parece sofisticado:
$repo = Mockery::mock(PedidoRepository::class);
$repo->shouldReceive('criar')->once()->andReturn(new Pedido());
$this->app->instance(PedidoRepository::class, $repo);
Se você mocka tudo que o código toca, o que sobra pra testar? Você afirmou que o método A chamou o método B. Se o B estiver quebrado, o teste continua verde felizinho.
No Laravel isso é ainda menos necessário que em outros frameworks: com RefreshDatabase e SQLite em memória, bater no banco de verdade custa milissegundos. Mocka o que é lento, externo ou imprevisível — API de terceiro, gateway de pagamento, relógio. O resto, deixa rodar.
Bônus: cobertura não é qualidade
78% de cobertura significa que 78% das linhas foram executadas durante os testes. Não que foram verificadas.
Aquele primeiro teste, o do assertStatus(200)? Ele executa o controller inteiro. Cobertura linda. Poder de detecção zero.
Se quiser uma métrica que não mente, procura por mutation testing — no PHP, o Infection. Ele altera seu código de propósito (troca um > por >=, inverte um booleano) e verifica se algum teste percebeu. Os que não perceberam são exatamente os inúteis.
É desconfortável rodar isso pela primeira vez. Vale.
Antes de você fechar a aba
Escolhe o teste mais importante da sua suíte agora. Vai no código que ele testa e apaga uma linha de regra de negócio.
Rodou verde? Você acabou de descobrir uma coisa importante sobre a sua rede de segurança.
E aí, qual foi o bug que passou batido pela sua suíte inteira e explodiu em produção? Conta nos comentários — eu começo: um Mail::fake() que eu deixei num teste que deveria checar o envio. Nunca mais. 😅
Top comments (0)