QA para desenvolvedores: mentalidade de quebra vs. construção
1. O dilema fundamental: quebrar ou construir?
1.1. A visão clássica do QA: caçador de bugs e validação destrutiva
O profissional de QA tradicional é treinado para pensar como um sabotador profissional. Sua missão não é provar que o software funciona, mas sim encontrar todas as maneiras pelas quais ele pode falhar. Essa mentalidade de "quebra" é essencial — sem ela, sistemas vão para produção com falhas grotescas em casos de borda.
Considere o exemplo clássico de validação de entrada:
Entrada esperada: número inteiro entre 1 e 100
Testes de um QA com mentalidade de quebra:
- Valor 0 (limite inferior inválido)
- Valor 101 (limite superior inválido)
- Valor -1 (negativo)
- String vazia ""
- Caractere especial "@"
- Número decimal 50.5
- Overflow: 9999999999999999999
- SQL injection: "1; DROP TABLE usuarios; --"
1.2. A visão do desenvolvedor: foco na entrega e na funcionalidade
O desenvolvedor, por outro lado, está focado em construir. Seu cérebro opera no modo "caminho feliz": dado A, faça B, retorne C. O viés de confirmação é natural — escrevemos testes que passam, não testes que falham.
// Teste típico de desenvolvedor:
function testSoma() {
resultado = soma(2, 3);
assert(resultado == 5); // Apenas o caminho feliz
}
// O que um QA adicionaria:
function testSoma_limites() {
assert(soma(0, 0) == 0);
assert(soma(-1, 1) == 0);
assert(soma(Number.MAX_VALUE, 1) == Infinity);
assert(soma(null, 5) throws TypeError);
}
1.3. Por que esses dois mundos precisam se encontrar
Quando QA e desenvolvedor trabalham em silos, o resultado é previsível: o dev entrega código frágil, o QA encontra dezenas de bugs, o prazo estoura, a culpa é distribuída. A integração dessas mentalidades produz código mais robusto desde o primeiro commit.
2. Mentalidade de quebra: o valor do ceticismo saudável
2.1. Testar para falhar: casos de borda e entradas inválidas
A arte de pensar como um QA envolve fazer perguntas incômodas: "O que acontece se o usuário fizer exatamente o que não deveria?".
Cenário: função que calcula desconto
Caminho feliz: compra > R$100 → 10% de desconto
Casos de borda (mentalidade de quebra):
- Compra = R$100,00 → desconto deve ser 10%? Ou apenas acima de R$100?
- Compra = R$0,01 → desconto zero?
- Compra negativa → erro ou valor absoluto?
- Múltiplos descontos acumulados → ordem de aplicação?
- Cupom expirado → mensagem clara ou crash silencioso?
2.2. Ferramentas e técnicas para provocar falhas intencionais
Fuzzing é uma técnica poderosa: alimentar o sistema com dados aleatórios, malformados ou inesperados para descobrir falhas que testes manuais jamais encontrariam.
Exemplo de fuzzing simples em Python:
import random
import string
def fuzz_input():
return ''.join(random.choices(
string.ascii_letters + string.digits + "!@#$%^&*()",
k=random.randint(0, 100)
))
for _ in range(1000):
try:
processar_dados(fuzz_input())
except Exception as e:
registrar_falha(f"Input: {fuzz_input()}, Erro: {e}")
2.3. O perigo do excesso: quando quebrar vira paranoia
Há um ponto em que a mentalidade de quebra se torna contraproducente. Testes que simulam cenários absurdos (como queda de rede em um sistema monolítico local) ou que exigem cobertura de 100% dos casos de erro imagináveis geram retrabalho e código inchado. O equilíbrio está em testar o que é provável e crítico, não o que é teoricamente possível.
3. Mentalidade de construção: o design orientado à qualidade
3.1. Escrever código testável desde o início
Código testável não é um extra — é um requisito de design. Injeção de dependências e baixo acoplamento permitem isolar unidades para teste sem mockar o mundo inteiro.
// Código não testável:
class PedidoService {
private Database db = new Database(); // Acoplamento rígido
public void criarPedido(Pedido p) {
db.salvar(p);
EmailService.enviar(p); // Dependência concreta
}
}
// Código testável:
class PedidoService {
private Database db;
private EmailService email;
public PedidoService(Database db, EmailService email) {
this.db = db; // Injeção permite mock
this.email = email;
}
}
3.2. Testes como documentação viva: BDD
Testes escritos em linguagem de domínio (como Gherkin) servem tanto como especificação quanto como validação. Eles documentam o comportamento esperado de forma legível para todos os stakeholders.
Feature: Cálculo de frete
Scenario: Frete grátis para compras acima de R$200
Given um carrinho com valor total de R$250
When o frete for calculado
Then o valor do frete deve ser R$0
Scenario: Frete proporcional para compras menores
Given um carrinho com valor total de R$50
And o CEP de destino for "12345-678"
When o frete for calculado
Then o valor do frete deve ser R$15
3.3. O viés de confirmação do desenvolvedor
Estudos mostram que desenvolvedores tendem a escrever testes que confirmam que o código funciona, não que testam sua robustez. Um exercício prático: antes de escrever o código, escreva 5 testes que devem falhar — isso força o cérebro a sair do modo construção e entrar no modo quebra.
4. O ponto de equilíbrio: QA como habilidade transversal
4.1. Desenvolvedor QA-ready
Um desenvolvedor QA-ready não espera o QA encontrar bugs — ele antecipa. Isso significa:
- Testar o "quase certo": e se o banco cair? E se a API retornar 500?
- Escrever testes de integração que validam contratos, não apenas lógica interna
- Revisar o próprio código com "óculos de QA"
4.2. Pirâmide de testes revisitada
A pirâmide clássica (muitos unitários, poucos e2e) ganha uma nova camada com a mentalidade de quebra:
Topo: Testes e2e (poucos, críticos)
Meio: Testes de integração (médios, contratos)
Base: Testes unitários (muitos, lógica)
Fundação: Testes de borda/fuzzing (contínuos, exploratórios)
Cada nível tem seu papel: unitários garantem lógica, integração garantem comunicação, e2e garantem fluxos completos, e testes de borda garantem resiliência.
4.3. Pair testing entre dev e QA
Sessões de pair testing (dev + QA) são extremamente produtivas. O QA sugere cenários destrutivos; o dev implementa correções em tempo real. O resultado é aprendizado mútuo e código mais robusto em minutos, não em semanas.
5. Armadilhas comuns na transição de mentalidade
5.1. Cobertura de código não é qualidade
Ter 90% de cobertura não significa nada se os 10% não testados contêm a lógica crítica de segurança. Métricas de cobertura devem ser usadas como indicadores, não como metas.
5.2. Testes que nunca falham
Se um teste nunca falha, ele não está testando nada. Testes "verdes o tempo todo" geralmente indicam uma de duas coisas: o sistema é imutável (improvável) ou o teste é fraco demais para detectar mudanças.
// Teste inútil:
function testSoma() {
assert(soma(2,2) == 4); // Sempre passará se a função existir
}
// Teste útil:
function testSoma_mudanca() {
const resultadoAntigo = soma(2,2);
// Após refatoração, verificar se comportamento mudou
assert(soma(2,2) == resultadoAntigo);
}
5.3. O custo de ignorar a mentalidade de quebra
Bugs em produção custam caro: perda de receita, dano à reputação, horas extras de suporte. Um bug encontrado em produção custa 10x mais que o mesmo bug encontrado em testes, e 100x mais que o mesmo bug evitado no design.
6. Métricas que importam: medindo o impacto da mudança
6.1. Taxa de escape de bugs vs. velocidade de entrega
A métrica mais importante é: quantos bugs chegam a produção dividido pelo número de features entregues. Uma taxa decrescente indica que a mentalidade de quebra está funcionando sem sacrificar velocidade.
6.2. Lead time para correção
Quanto tempo leva entre um bug ser reportado e corrigido? Times que integram QA e dev reduzem esse tempo drasticamente — o dev já conhece o contexto do código e o QA já documentou o cenário exato.
6.3. Dashboard pessoal de saúde do código
Cada desenvolvedor pode manter métricas simples:
- Número de testes que falharam na última semana
- Taxa de aprovação em code review (quantas revisões pediram mudanças)
- Bugs reportados vs. bugs encontrados por testes próprios
7. Cultura de feedback e melhoria contínua
7.1. Post-mortems sem culpa
Quando um bug escapa para produção, o foco não deve ser "quem errou", mas "como o processo permitiu que isso acontecesse". Post-mortems sem culpa incentivam a transparência e o aprendizado.
7.2. Code review orientado a testes
Durante code review, pergunte: "Onde estão os testes para os casos de erro? O que acontece se a entrada for inesperada? Esse código é testável isoladamente?".
7.3. Do "meu código funciona" para "nosso sistema é resiliente"
A mudança cultural mais importante é passar do orgulho individual ("meu código funciona") para a responsabilidade coletiva ("nosso sistema é resiliente"). Isso exige que cada desenvolvedor incorpore a mentalidade de QA como parte natural do seu fluxo de trabalho, não como uma etapa separada.
O equilíbrio entre quebrar e construir não é um compromisso — é uma síntese. Quebrar sem construir gera caos. Construir sem quebrar gera fragilidade. Apenas quando ambas as mentalidades coexistem é que entregamos software que realmente merece ir para produção.
Referências
- Mindset de QA: Como pensar como um tester sendo desenvolvedor — Artigo do Ministry of Testing sobre como desenvolvedores podem adotar a mentalidade de teste exploratório.
- Pirâmide de Testes de Mike Cohn — Martin Fowler explica a pirâmide de testes clássica e como aplicá-la na prática.
- Fuzzing: Guia da OWASP para Testes de Segurança — Documentação oficial da OWASP sobre técnicas de fuzzing para descobrir vulnerabilidades.
- BDD e SpecFlow: Especificações Executáveis — Guia oficial do SpecFlow sobre Behavior-Driven Development e como criar testes que funcionam como documentação viva.
- Post-mortems sem culpa no Google SRE — Capítulo do Google SRE Book sobre como realizar post-mortems que focam em aprendizado, não em culpa.
- Testes de Limite e Casos de Borda — Tutorial prático sobre análise de valor limite e particionamento de equivalência para testes de software.