Qualidade¶
Fundamentos: qualidade e teste de software¶
O que é qualidade de software¶
Qualidade é perceptível, mas difícil de medir. Uma definição prática: tudo o que o cliente quer, funcionando da forma desejada, dentro dos prazos e custos acordados. Cada parte falha com facilidade: o cliente nem sempre consegue expressar o que quer; a equipe nem sempre entende ou implementa corretamente (e há problemas de ambiente e de plataforma); e a soma disso gera retrabalho, atraso e custo.
O que ajuda a atingir qualidade (não existe uma única bala de prata):
| Elemento | Papel |
|---|---|
| Métodos de Engenharia de Software | Processos e modelos de desenvolvimento (Engenharia de Software) |
| Técnicas e revisões formais | Inspeções de requisitos, design e código; base da verificação e validação |
| Padrões e procedimentos | Convenções de codificação, padrões de projeto, análise estática automática (SonarQube, Checkstyle, SpotBugs), integração contínua |
| Garantia de qualidade (QA) | Pessoa ou equipe que decide o que aplicar e quando, pois aplicar tudo estoura custo e prazo |
| Medições | Sem medir não se sabe se está funcionando (taxa de defeitos, cobertura, problemas achados em revisão) |
| Normas e modelos de qualidade | Guias de processo como CMMI e a família ISO (9001/12207/25010) |
| Testes | A atividade que mais consome tempo e mais evidências de qualidade produz |
Definição: verificação e validação (V&V)
Validação: "estamos construindo o software certo?" — os requisitos refletem o que o usuário precisa? (checa validade, consistência, completude e realismo, por revisão, prototipação e geração de casos de teste a partir dos requisitos; é manual e envolve analistas, desenvolvedores e testadores). Verificação: "estamos construindo o software certo da forma certa?" — o código reflete os requisitos? Pode ser estática (sem executar: revisão e análise de código) ou dinâmica (executando: testes funcionais e não funcionais).
Modelos de atributos de qualidade¶
- Fatores de McCall, em três perspectivas: operação (correção, confiabilidade, usabilidade, integridade, eficiência), revisão (manutenibilidade, flexibilidade, testabilidade) e transição (portabilidade, reusabilidade, interoperabilidade).
- ISO/IEC 9126 (atual ISO/IEC 25010), com seis atributos: funcionalidade (adequação, acurácia, interoperabilidade, segurança), confiabilidade (maturidade, tolerância a falhas, recuperabilidade), usabilidade (inteligibilidade, apreensibilidade, atratividade, acessibilidade), eficiência (tempo de resposta, consumo de recursos), manutenibilidade (analisabilidade, modificabilidade, estabilidade, testabilidade) e portabilidade (adaptabilidade, instalabilidade, substituibilidade).
Esses atributos dizem o que avaliar e ajudam a escolher os tipos de teste (mapeamento mais abaixo); os não funcionais são detalhados em System Design.
Conceitos de teste¶
Definição: erro, defeito e falha
Erro é o engano humano (da pessoa que especifica ou programa); defeito (bug) é o erro materializado no software; falha é o comportamento incorreto observado quando o defeito é executado. Nem todo defeito causa falha de imediato.
Um caso de teste é um cenário de execução com seus dados de entrada e resultado esperado. O ponto de verificação é o que se confere no resultado, e o critério de teste é a estratégia que orienta o que testar, como avaliar e quais dados usar (testar todas as combinações é inviável). Estratégias de seleção:
| Foco | Estratégia | Ideia |
|---|---|---|
| Dados | Particionamento de equivalência | Divide as entradas em classes válidas e inválidas; um valor de cada classe representa todas (ex.: idade válida de 18 a 45; inválidas: < 18 e > 45) |
| Dados | Análise de valores-limite | Testa as bordas das classes (17, 18, 45, 46), onde os defeitos se concentram |
| Regras de negócio | Tabela de decisão | Combina condições (causas) e ações (efeitos); cada coluna vira um caso de teste |
| Regras de negócio | Pairwise (combinatório) | Cobre todos os pares de valores com poucos casos, quando as combinações explodem (por exemplo, de 36.000 combinações reduz-se a algumas dezenas) |
| Seleção | Baseado em modelo | Descreve o comportamento num modelo e gera casos, saídas esperadas e comparação a partir dele |
| Seleção | Baseado em caso de uso/história de usuário | Cada fluxo (principal, alternativo, de erro) vira um caso de teste |
| Seleção | Exploratório | Sem roteiro fixo: a pessoa aprende o software enquanto testa, em sessões curtas (útil quando não há especificação clara) |
As três dimensões do teste¶
Todo teste responde a três perguntas ao mesmo tempo:
flowchart LR
T((Teste)) --> C[Como?<br/>técnica]
T --> Q[Quando?<br/>nível]
T --> O[O quê?<br/>tipo]
C --> C1[Caixa branca / preta / cinza]
Q --> Q1[Unidade, integração, sistema, aceitação]
O --> O1[Funcional, regressão, desempenho,<br/>usabilidade, segurança, acessibilidade, portabilidade]
1. Técnicas (como):
| Técnica | Visão | Foco |
|---|---|---|
| Caixa branca (estrutural) | Conhece o código | Caminhos, desvios e linhas executadas (cobertura) |
| Caixa preta (funcional) | Só entradas e saídas, a partir dos requisitos | Comportamento esperado |
| Caixa cinza | Parte do conhecimento interno | Mistura as duas |
2. Níveis (quando):
| Nível | O que verifica | Quem executa | Observação |
|---|---|---|---|
| Unidade | A menor parte (método, função) isolada | Quem desenvolve | Exercita cada linha, cada desvio e as interações (com dublês); veja as seções seguintes |
| Integração | Se as unidades trocam informação corretamente (camadas, banco, APIs externas) | Desenvolvimento | Mais robusto e complexo que o de unidade |
| Sistema | O software completo, integrado, como um usuário final, contra os requisitos funcionais e não funcionais | Equipe independente de testes, não quem codificou | Manual (roteiros) ou automatizado (Selenium, Cypress); use automação para fluxos grandes e repetitivos |
| Aceitação | Se o software atende ao que o cliente queria, perto de entrar em produção | Cliente ou usuários | Fase alfa (ambiente de desenvolvimento, com a equipe) e beta (ambiente real, usuários reais); em geral manual |
3. Tipos (o quê):
| Tipo | Foco |
|---|---|
| Funcional | O comportamento segue a especificação. O teste de fumaça (smoke) verifica só as funções principais e críticas |
| Regressão | Mais uma estratégia do que um tipo: reexecutar os testes após cada mudança para garantir que nada que funcionava quebrou; costuma ser automatizada |
| Desempenho | Carga (picos previstos e concorrência), estresse (situações além do previsto, ou infraestrutura reduzida, até a quebra e a recuperação) e volume (grande quantidade de dados no banco). Ferramenta típica: JMeter. Meça gargalos (conexões, banco, processos) |
| Usabilidade | Facilidade de uso. Heurísticas: visibilidade do status, mensagens de erro claras, consistência... Métodos: rastreamento ocular (eye tracking), testes com protótipos, tarefas predefinidas; moderados x não moderados; presenciais x remotos |
| Segurança | SAST (análise estática do código) + DAST (ataque simulado ao sistema em execução) são parceiros, não concorrentes; red/blue/purple team; pentest (aplicação, rede, hardware) com base no OWASP Top 10 e em normas como PCI DSS — veja Segurança |
| Acessibilidade | Uso por qualquer pessoa, com ou sem deficiência. Diretrizes WCAG (W3C) em níveis A, AA e AAA, organizadas em quatro princípios — perceptível, operável, compreensível e robusto: texto alternativo, legendas, navegação por teclado, contraste, tempo suficiente, previsibilidade, ajuda no preenchimento, compatibilidade com leitores de tela; use ferramentas automáticas |
| Portabilidade | Funciona em diferentes navegadores, sistemas e dispositivos |
Mapeamento aproximado entre tipos de teste e atributos de qualidade: funcional → correção, confiabilidade (McCall) e funcionalidade (ISO); regressão → manutenibilidade; desempenho → eficiência; usabilidade → usabilidade; segurança → integridade, confiabilidade; portabilidade → portabilidade. A testabilidade e a reusabilidade não são "testadas": são características do software que tornam o teste possível.
Por que testes automatizados¶
Um teste manual — abrir a aplicação, preencher um formulário, clicar num botão, conferir se o resultado bate com o esperado — funciona, mas não escala: é lento, custa caro (tempo de uma pessoa) e é chato o suficiente para que ninguém queira repeti-lo toda vez que uma linha de código muda.
Definição: Teste automatizado
Um programa que testa outro programa — monta um cenário, executa a ação que se quer testar e verifica se a saída bate com o esperado, tudo sem intervenção manual. O mesmo raciocínio de um teste manual, só que quem executa é a máquina: mais rápida, incansável, e sem esquecer nenhum passo.
Definição: Arrange, Act, Assert (organizar, agir, verificar)
Os três passos que praticamente todo teste automatizado segue, na ordem: monta o cenário (cria os objetos e o estado inicial necessário), executa a ação que se quer testar, e valida a saída (compara o resultado obtido com o esperado). Um teste que não segue essa estrutura tende a ficar confuso de ler.
JUnit: automatizando a validação¶
Escrever o cenário e a ação já é automatizável com código Java comum — o que falta é automatizar a validação (comparar resultado obtido com o esperado) e relatar, resultado por resultado, quais passaram e quais falharam. É esse o papel do JUnit, o framework de testes de unidade mais popular do mundo Java.
import static org.junit.Assert.assertEquals;
public class AvaliadorTest {
@Test
public void deveEntenderLancesEmOrdemCrescente() {
// cenário
Usuario joao = new Usuario("Joao");
Usuario jose = new Usuario("José");
Usuario maria = new Usuario("Maria");
Leilao leilao = new Leilao("Playstation 3 Novo");
leilao.propoe(new Lance(maria, 250.0));
leilao.propoe(new Lance(joao, 300.0));
leilao.propoe(new Lance(jose, 400.0));
// ação
Avaliador leiloeiro = new Avaliador();
leiloeiro.avalia(leilao);
// validação
assertEquals(400, leiloeiro.getMaiorLance(), 0.0001);
assertEquals(250, leiloeiro.getMenorLance(), 0.0001);
}
}
Definição: Regras de um método de teste (JUnit)
Um método de teste precisa ser público, de instância (não static), sem
parâmetros, e anotado com @Test. O JUnit descobre e executa automaticamente todo
método marcado assim, mostrando o resultado numa barra verde (tudo passou) ou
vermelha (algo falhou) — junto com a mensagem de erro exata de qual asserção não
bateu, sem precisar ler System.out.println manualmente.
Assert.assertEquals(esperado, calculado) é o método central de validação — repare na
ordem dos parâmetros: esperado primeiro, calculado depois. Isso importa porque, se a
asserção falhar, a mensagem de erro do JUnit usa essa ordem para descrever a diferença;
invertidos, a mensagem sai enganosa. É comum importar assertEquals de forma estática
(import static org.junit.Assert.assertEquals;) para escrever assertEquals(...) direto,
sem o prefixo Assert..
Convenções na escrita de testes¶
- Nome da classe de teste:
NomeDaClasseTestada+ sufixoTest(AvaliadorTestpara testarAvaliador) — nunca "testes" ou "TesteDaClasse". A convenção deixa óbvio, ao olhar qualquer classe, onde estão os testes dela. - Nome do método de teste: descreva o comportamento testado
(
deveEntenderLancesEmOrdemCrescente), nunca genérico (main,teste1) — o nome do método é o que aparece na tela do executor de testes quando algo falha; um nome vago obriga a ler o código do teste inteiro só para entender o que quebrou. - Separação física: testes vivem numa pasta de código-fonte separada da classe de
produção (em Java, um source folder dedicado), mas no mesmo pacote — isso permite
ao teste enxergar também métodos com visibilidade padrão (default), não só os
public.
Definição: Prefira um assert por comportamento, não por linha
A recomendação popular de "um único assert por teste" é forte demais na prática: um comportamento de um objeto (a unidade real de um sistema OO) frequentemente altera vários atributos ao mesmo tempo — vale ter várias asserções sobre o mesmo objeto, desde que todas validem a mesma ação. O que deve ser evitado é testar mais de um comportamento diferente dentro do mesmo método — isso sim deve virar dois testes separados.
Você é mais produtivo escrevendo testes, não menos¶
Um argumento comum contra testes automatizados é a produtividade — "eu já testaria manualmente, por que perder tempo escrevendo um teste também?". A resposta prática: depende de como se mede produtividade. Se for por linhas de produção escritas por dia, escrever testes parece custar tempo. Mas quem escreve testes só executa manualmente uma vez (na hora de escrever o teste); depois disso, a suíte inteira roda em segundos, quantas vezes for preciso — enquanto quem não escreve testes repete o teste manual, do zero, a cada mudança, para sempre.
Cobrindo cenários: classes de equivalência¶
Um único teste passando garante muito pouco — cobre só o cenário exato que ele descreve. Testar todas as combinações possíveis de entrada é impossível na prática (e encher a suíte de testes quase idênticos também tem custo: mais testes para manter). A saída é agrupar cenários equivalentes entre si, e escrever só um teste por grupo.
Definição: Classe de equivalência
Um grupo de cenários de entrada que, sob a ótica do comportamento sendo testado, são
equivalentes — testar qualquer um deles testa, na prática, todos os outros do
mesmo grupo. Para um método que ordena lances, por exemplo, (250, 300, 400) e
(1000, 2000, 3000) pertencem à mesma classe de equivalência ("lances em ordem
crescente") — o segundo teste não agrega cobertura nova, só repete o primeiro com
números diferentes. Já "lances em ordem decrescente", "lances em ordem aleatória" e
"apenas um lance" são classes de equivalência diferentes, cada uma merecendo seu
próprio teste.
Casos especiais e limites¶
Definição: Caso especial (edge case)
Um cenário na borda do comportamento — a lista com um único elemento (separado do
caso "lista com vários"), a lista vazia, o valor exatamente igual ao limite de uma
condição. Um if (salario >= 2000) tem, na prática, três classes de equivalência
diferentes: salário menor que 2000, maior que 2000, e exatamente igual a 2000 — é
fácil confundir > com >= na implementação, e só um teste no valor-limite exato
pega esse tipo de erro.
A rede de segurança contra regressão¶
Definição: Teste de regressão
Qualquer teste antigo que continua rodando conforme o sistema ganha funcionalidades novas, garantindo que uma mudança recente não quebrou um comportamento que já funcionava. É o maior ganho prático de uma suíte de testes: sem ela, o desenvolvedor testa manualmente só a funcionalidade nova, e comportamentos antigos que quebraram silenciosamente só aparecem quando o usuário (ou o cliente) encontra o bug em produção — bem mais tarde, e bem mais caro de corrigir.
Isso muda a relação do time com o próprio código: mexer num algoritmo já existente deixa de ser arriscado às cegas — se a mudança quebrar algo que os testes cobrem, o teste avisa imediatamente, no computador de quem fez a mudança, não meses depois em produção.
Cuidando dos testes¶
Código de teste é código — e sofre dos mesmos problemas de manutenção que código de
produção mal cuidado. Se o construtor de Avaliador mudar (ganhar um parâmetro novo,
por exemplo), e essa instanciação estiver repetida em todos os métodos de teste, a
mudança se propaga para todos eles — o mesmo problema de duplicação já discutido em
Boas Práticas.
Definição: @Before / @After / @BeforeClass / @AfterClass (JUnit)
Anotações para código que roda ao redor de cada teste, sem precisar repeti-lo em
cada método: @Before roda antes de cada método @Test (ideal para montar o
cenário comum a vários testes — trocar um método auxiliar private por @Before
faz o JUnit chamá-lo automaticamente); @After roda depois de cada teste (limpar
recursos usados, como conexões ou arquivos abertos). @BeforeClass/@AfterClass
seguem a mesma ideia, mas rodam uma única vez para a classe inteira — úteis para
um recurso caro de inicializar que pode ser reaproveitado por todos os testes.
public class AvaliadorTest {
private Avaliador leiloeiro;
private Usuario joao, jose, maria;
@Before
public void criaAvaliador() {
this.leiloeiro = new Avaliador();
this.joao = new Usuario("Joao");
this.jose = new Usuario("José");
this.maria = new Usuario("Maria");
}
@Test
public void deveEntenderLancesEmOrdemCrescente() {
Leilao leilao = new Leilao("Playstation 3 Novo");
leilao.propoe(new Lance(joao, 250.0));
leilao.propoe(new Lance(jose, 300.0));
leilao.propoe(new Lance(maria, 400.0));
leiloeiro.avalia(leilao);
assertEquals(400.0, leiloeiro.getMaiorLance(), 0.00001);
assertEquals(250.0, leiloeiro.getMenorLance(), 0.00001);
}
}
Test Data Builders¶
Montar um cenário complexo (várias linhas para criar um Leilao com vários lances)
repetido em cada teste tem o mesmo problema de duplicação — a solução é isolar essa
criação numa classe dedicada.
Definição: Test Data Builder
Padrão de projeto para código de teste: uma classe cuja única responsabilidade é
construir um objeto complexo para uso em testes, com uma API fluente (métodos
encadeados, cada um devolvendo this) que deixa a montagem do cenário muito mais
legível que instanciar e configurar o objeto manualmente em cada teste.
public class CriadorDeLeilao {
private Leilao leilao;
public CriadorDeLeilao para(String descricao) {
this.leilao = new Leilao(descricao);
return this;
}
public CriadorDeLeilao lance(Usuario usuario, double valor) {
leilao.propoe(new Lance(usuario, valor));
return this;
}
public Leilao constroi() {
return leilao;
}
}
@Test
public void deveEncontrarOsTresMaioresLances() {
Leilao leilao = new CriadorDeLeilao()
.para("Playstation 3 Novo")
.lance(joao, 100.0)
.lance(maria, 200.0)
.lance(joao, 300.0)
.lance(maria, 400.0)
.constroi();
leiloeiro.avalia(leilao);
// ... asserts
}
O ganho de legibilidade é grande, e, se a forma de construir um Leilao mudar no
futuro, só o builder precisa ser ajustado — não todos os testes que o utilizam.
Definição: Acoplamento entre testes e produção
Código de teste depende diretamente do código de produção (construtores, métodos públicos) — uma mudança no código de produção pode forçar mudança em muitos testes de uma vez. Métodos auxiliares e Test Data Builders isolam esse acoplamento num único lugar, então uma mudança na forma de construir um objeto exige ajustar só o builder, não cada teste individualmente.
Testando exceções¶
Um teste cujo cenário deveria lançar uma exceção não pode simplesmente chamar
assertEquals depois da ação — a execução nunca chega lá, porque a exceção interrompe o
método antes. Duas formas de testar isso:
// abordagem manual: try/catch + Assert.fail()
@Test
public void naoDeveAvaliarLeiloesSemNenhumLanceDado() {
Leilao leilao = new CriadorDeLeilao().para("Playstation 3 Novo").constroi();
try {
leiloeiro.avalia(leilao);
Assert.fail(); // se chegou aqui, a exceção NÃO foi lançada — teste deve falhar
} catch (RuntimeException e) {
// deu certo — a exceção esperada foi lançada
}
}
// abordagem recomendada (JUnit 4+): atributo "expected" do @Test
@Test(expected = RuntimeException.class)
public void naoDeveAvaliarLeiloesSemNenhumLanceDado() {
Leilao leilao = new CriadorDeLeilao().para("Playstation 3 Novo").constroi();
leiloeiro.avalia(leilao);
}
Definição: @Test(expected = ...)
Informa ao JUnit que aquele método só passa se a exceção informada for lançada
durante sua execução — falha tanto se nenhuma exceção for lançada quanto se uma
exceção de tipo diferente for lançada. Mais legível e direto que o try/catch +
Assert.fail() manual, que funciona mas é mais verboso.
Legibilidade: Hamcrest¶
assertEquals(esperado, calculado) exige pensar na ordem "não natural" (o valor
calculado primeiro seria mais intuitivo de ler em voz alta). O projeto Hamcrest
(comumente usado junto do JUnit) oferece uma sintaxe mais próxima de uma frase em
inglês:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
assertThat(leiloeiro.getMenorLance(), equalTo(250.0));
// lê-se: "garanta que o menor lance é igual a 250.0"
assertThat(maiores, hasItems(
new Lance(maria, 400.0),
new Lance(joao, 300.0),
new Lance(maria, 200.0)
));
// lê-se: "garanta que 'maiores' tem estes itens" — requer equals() bem implementado
// na classe comparada (aqui, Lance), senão o matcher não consegue comparar corretamente
Cobertura de testes: nem sempre 100%¶
Definição: Cobertura de código (code coverage)
Métrica que indica a porcentagem do código-fonte que é executada por pelo menos um teste automatizado. Uma cobertura de 90% significa que 10% do código nunca é exercitado por nenhum teste.
Perseguir 100% de cobertura como meta absoluta não costuma valer a pena: código trivial (getters/setters gerados pela IDE, sem lógica nenhuma) raramente quebra e raramente precisa de teste dedicado. Cobertura é útil como indicador de onde faltam testes — principalmente em trechos complexos e importantes —, não como prova de que o sistema está livre de defeitos (100% de cobertura não impede um teste de verificar a coisa errada, ou de faltar um cenário que a métrica não capta).
Test-Driven Development (TDD)¶
Até aqui, os testes foram escritos depois do código de produção, para validá-lo. Existe uma forma diferente de trabalhar, em que o teste vem antes — o que muda, nessa inversão, é mais profundo do que parece à primeira vista.
Definição: Test-Driven Development (TDD)
Ciclo de desenvolvimento repetido a cada pequeno incremento de funcionalidade: (1) escreve-se um teste que ainda falha (a funcionalidade não existe); (2) escreve-se o código de produção mais simples possível que faz esse teste passar, mesmo que a implementação não seja elegante ainda; (3) com o teste passando, refatora-se o código para ficar mais claro/simples, apoiado na segurança de que o teste avisa imediatamente se a refatoração quebrar algo. O ciclo é popularmente chamado de vermelho → verde → refatorar.
graph LR
R["1. Escreve um teste
(vermelho: falha)"] --> G["2. Código mais simples
possível (verde: passa)"]
G --> Ref["3. Refatora
(continua verde)"]
Ref --> R
Definição: Por que ver o teste falhar primeiro
Rodar o teste antes de escrever o código de produção, e confirmar que ele falha como esperado, garante que o teste está realmente testando o comportamento certo — um teste automatizado também é código, e pode estar implementado de forma incorreta (por exemplo, sempre passando, não importa o que aconteça). Só depois de ver o vermelho é que se escreve a implementação mais simples que faz o teste ficar verde.
Efeitos no design de classes¶
A prática de TDD tem um efeito colateral discutido por autores como Kent Beck, Martin Fowler e Michael Feathers: sistemas construídos com TDD tendem a ter melhor design de classes do que sistemas equivalentes sem TDD. A explicação: escrever o teste primeiro naturalmente empurra o desenvolvedor a escrever um código fácil de testar — e código fácil de testar tende a ter características que também são, de forma independente, sinais de bom design:
- Mais coeso — código que faz muita coisa ao mesmo tempo é mais difícil de isolar num teste.
- Menos acoplado — código fortemente acoplado a outras dependências é mais difícil de testar sem montar o sistema inteiro.
- Interface pública simples — não dá vontade de invocar 10 métodos só para conseguir testar um comportamento.
- Pré-condições simples — não dá vontade de montar cenários enormes só para conseguir chamar um método.
Definição: Devo testar métodos privados?
Não. Se surge vontade de testar um método privado isoladamente do resto da classe, isso é um sinal de que aquele trecho de código está no lugar errado — a solução não é forçar acesso ao método privado no teste, é extrair aquele código para uma classe própria (com seus próprios métodos públicos, testáveis normalmente).
Baby steps¶
Definição: Baby steps
A prática de, no ciclo de TDD, sempre escrever o código mais simples possível que faz o teste atual passar — mesmo que pareça ingênuo demais para o cenário final (retornar uma constante fixa, por exemplo, antes de implementar a lógica real). Passos pequenos tornam mais fácil perceber, a cada momento, exatamente qual mudança fez o teste falhar ou passar — e o código simples do início vai sendo generalizado conforme testes novos (cobrindo mais cenários) forem sendo adicionados e forçarem a implementação a evoluir.
Escrever passos maiores que a confiança no trecho de código permite tende a ser contraproducente — Kent Beck (criador da prática) recomenda ajustar o tamanho do passo à confiança que se tem naquele código: passos maiores quando o terreno é bem conhecido, passos pequenos quando não é. O padrão, porém, deve ser o passo pequeno — passos grandes são a exceção, não a regra.
Definição: TDD o tempo todo?
Não é uma prática universal para toda situação — faz sentido ao implementar funcionalidades novas, corrigir bugs, ou mexer em código complexo, mas nem sempre vale o ciclo completo (às vezes o objetivo é só escrever os testes que faltam para uma funcionalidade já pronta, sem TDD). Em código extremamente simples, a prática pode ser dispensável — mas vale desconfiar sempre que algo "parece simples demais para testar": é justamente aí que bugs se escondem.
Mock Objects¶
Os testes vistos até aqui cobriam classes de domínio "puras" — sem dependência de banco
de dados, rede ou qualquer sistema externo. Uma classe que depende de
infraestrutura (ex.: um EncerradorDeLeilao que usa um LeilaoDao para buscar e
persistir leilões) parece, à primeira vista, exigir um banco de dados de verdade para
ser testada — e testar contra um banco real tem custo alto: é lento (uma consulta real
demora mais que código em memória), frágil (o teste quebra se o estado do banco não for
limpo entre execuções) e exige infraestrutura só para rodar a suíte.
Definição: Mock Object
Um objeto que finge ser outro objeto do sistema, simulando o comportamento esperado dele sem executar a implementação real — em geral usado para substituir uma dependência que fala com um sistema externo (banco, rede, relógio do sistema). Já apresentado, em alto nível, em Boas Práticas como consequência natural de OCP/DIP — esta seção aprofunda como criar e usar mocks na prática, com o Mockito, um dos frameworks de mock mais populares do mundo Java.
import static org.mockito.Mockito.*;
// criando um mock de LeilaoDao
LeilaoDao daoFalso = mock(LeilaoDao.class);
// ensinando o mock a reagir de uma certa forma quando invocado
when(daoFalso.correntes()).thenReturn(leiloesAntigos);
// usando o mock no lugar da dependência real
EncerradorDeLeilao encerrador = new EncerradorDeLeilao(daoFalso);
encerrador.encerra();
Definição: mock() / when() / thenReturn() (Mockito)
mock(Classe.class) cria um objeto falso daquela classe (ou interface) — todos os
métodos, por padrão, não fazem nada e devolvem um valor "vazio" (null, 0,
false, conforme o tipo de retorno). when(mock.metodo(args)).thenReturn(valor)
ensina o mock: sempre que metodo for chamado com aqueles argumentos específicos,
devolva valor em vez do comportamento padrão. É assim que se cria um "banco de
dados de mentira" sem precisar de um banco de verdade rodando.
Para que a classe sob teste use o mock em vez da implementação real, ela precisa
receber a dependência de fora (pelo construtor, tipicamente) — o mesmo ponto já
discutido no OCP/DIP: só é possível mockar facilmente uma dependência que não é
instanciada (new) diretamente dentro da classe.
Verificando invocações: verify()¶
Nem todo comportamento testável é sobre retorno — às vezes o que importa é só
confirmar que um método foi chamado, mesmo que ele não devolva nada (void), como
dao.atualiza(leilao).
@Test
public void deveAtualizarLeiloesEncerrados() {
RepositorioDeLeiloes daoFalso = mock(RepositorioDeLeiloes.class);
when(daoFalso.correntes()).thenReturn(Arrays.asList(leilao1));
EncerradorDeLeilao encerrador = new EncerradorDeLeilao(daoFalso);
encerrador.encerra();
// verifica que o método foi realmente invocado, com aquele argumento específico
verify(daoFalso).atualiza(leilao1);
}
Definição: verify() (Mockito)
Confirma que um método do mock foi de fato invocado pelo código de produção,
opcionalmente checando com quais argumentos (verify(mock).metodo(argumentoEsperado)
só passa se o método tiver sido chamado exatamente com aquele argumento) e quantas
vezes (verify(mock, times(2)).metodo(); atLeastOnce() aceita uma ou mais
chamadas). Se o método esperado não tiver sido invocado — ou tiver sido invocado com
argumentos diferentes, ou um número de vezes diferente do esperado — o teste falha,
com uma mensagem detalhando o que era esperado e o que de fato aconteceu.
Definição: Mocks estritos e acoplamento ao teste
Usar verify() deixa explícito, no teste, quais métodos e em qual ordem o
código de produção invoca suas dependências — o que antes era um detalhe de
implementação encapsulado passa a "vazar" para dentro dos testes. Isso é uma troca
consciente: ganha-se confiança de que a interação entre classes está correta, mas o
teste fica mais acoplado à forma como a classe é implementada, não só ao que
ela produz — mudar a implementação (mesmo mantendo o comportamento externo idêntico)
pode quebrar testes que fazem verify() demais.
Por que métodos estáticos são difíceis de mockar¶
Definição: Métodos estáticos e testabilidade
Frameworks de mock tradicionais (como o Mockito) não conseguem simular métodos
static com a mesma facilidade que métodos de instância — método estático tem
"cheiro" de código procedural (chamado direto pela classe, sem uma instância
substituível por trás). Um método que parece um bom candidato a static vale
reconsiderar como método de instância, através de uma interface — isso abre a
possibilidade de mocká-lo no futuro, mesmo que hoje pareça desnecessário.
Simulando exceções¶
Mocks também servem para simular um cenário de erro de uma dependência — útil para testar como o código de produção reage quando, por exemplo, a conexão com o banco falha:
thenThrow(excecao) substitui thenReturn quando o objetivo é que o mock lance a
exceção informada, em vez de devolver um valor, no momento em que aquele método for
invocado no teste.
Capturando argumentos recebidos pelo mock¶
Além de verificar se um método foi chamado (verify), às vezes é útil capturar
o valor exato de um argumento passado a ele, para inspecioná-lo depois — usado com
o ArgumentCaptor do Mockito, útil quando o objeto passado é construído dinamicamente
dentro do próprio método testado (então não dá para simplesmente comparar com um valor
já conhecido de antemão).
Isolando para testar: criando abstrações¶
Uma classe cujo comportamento depende de algo que não é naturalmente mockável — por
exemplo, o relógio do sistema (new Date(), chamado direto dentro de um método) — não
tem como ser testada de forma determinística sem alguma mudança de design: encapsular
esse acesso atrás de uma interface própria (ex.: um Relogio com um método agora())
transforma uma dependência "invisível e fixa" em algo que pode ser injetado e mockado,
assim como qualquer outra dependência.
O que mockar, e o que não mockar?¶
Definição: Quando vale a pena mockar uma dependência
Mockar faz sentido para dependências lentas, não determinísticas, ou externas ao processo (banco de dados, chamadas de rede, sistema de arquivos, relógio do sistema) — coisas que tornariam o teste lento, instável (resultado diferente a cada execução) ou dependente de infraestrutura externa rodando. Classes de domínio simples, sem esse tipo de dependência, geralmente não precisam ser mockadas — testar com a implementação real é mais simples e não traz nenhuma das desvantagens acima.
Fake: outro tipo de dublê de teste¶
Definição: Fake x Mock
Nem todo "dublê de teste" (termo genérico para qualquer substituto de uma dependência
real usado só em testes) é um Mock Object criado dinamicamente por um framework. Um
Fake é uma implementação escrita à mão, mais simples que a real, que
sobrescreve o comportamento original de uma classe para permitir controlar
diretamente o que ela retorna — em vez de configurar expectativas com when(), o
comportamento fica explícito no próprio código da classe fake.
// fake: uma subclasse escrita à mão que sobrescreve o comportamento de rede
class ServicoLoginFake extends ServicoLogin {
@Override
public int autenticar(String idUsuario) {
if (idUsuario.equals("usuarioValido")) {
return 200;
}
return 404;
}
}
Um Fake costuma ser preferido a um Mock quando o comportamento a simular é usado em
muitos testes diferentes (evita repetir a mesma configuração de when()/thenReturn()
em cada teste) ou quando a lógica de simulação é complexa o suficiente para merecer sua
própria classe, em vez de expectativas avulsas espalhadas pelos testes.
Definição: WireMock
Ferramenta que simula um serviço HTTP externo, respondendo a requisições reais (não apenas chamadas de método em memória, como um Mock/Fake tradicional) com respostas pré-configuradas — útil para testar a integração com uma API de terceiros (ex.: um gateway de pagamento) sem depender dela estar disponível, sem custos de chamadas reais, e com controle total sobre cenários de sucesso, erro e latência. Costuma rodar como um container à parte no ambiente de testes/desenvolvimento.
Testes de integração¶
Nem toda dependência de infraestrutura deve ser mockada — em algum ponto, o próprio código que fala com o banco (o DAO/repositório) precisa ser testado contra um banco de verdade, senão um erro na consulta em si nunca seria descoberto.
Definição: Teste de integração
Teste que verifica a comunicação real entre a aplicação e um sistema externo (tipicamente um banco de dados) — diferente do teste de unidade (que isola a classe de qualquer dependência externa, mockando-as), o teste de integração deliberadamente não mocka a peça que está sendo integrada, porque é justamente essa integração que precisa ser validada.
Definição: Não mocke o que você está tentando testar
Mockar a própria API de acesso a dados (a Session/Query do Hibernate, por
exemplo) para "testar" um DAO permite que erros na consulta SQL/HQL em si
(nomes de coluna errados, cláusulas WHERE incorretas) passem despercebidos — o
mock nunca vai reclamar de uma consulta sintaticamente inválida, porque ele não
executa consulta nenhuma de verdade. Testar um DAO exige rodar contra um banco real
(ou, ao menos, um banco em memória compatível) — é exatamente o ponto em que mockar
deixa de fazer sentido.
Um teste de integração segue a mesma estrutura de um teste de unidade (monta cenário, executa ação, valida saída), mas tipicamente abre e fecha um recurso real (conexão, sessão) ao redor do teste:
public class UsuarioDaoTest {
private Session session;
private UsuarioDao usuarioDao;
@Before
public void antes() {
session = new CriadorDeSessao().getSession();
usuarioDao = new UsuarioDao(session);
}
@After
public void depois() {
session.close();
}
@Test
public void deveEncontrarPeloNomeEEmail() {
Usuario novoUsuario = new Usuario("João da Silva", "joao@dasilva.com.br");
usuarioDao.salvar(novoUsuario);
Usuario usuarioDoBanco = usuarioDao.porNomeEEmail("João da Silva", "joao@dasilva.com.br");
assertEquals("João da Silva", usuarioDoBanco.getNome());
}
}
Definição: flush() em testes de integração
Força o ORM a enviar imediatamente ao banco as operações pendentes (em vez de esperar o momento que ele julgar ideal) — importante em testes porque garante que a consulta seguinte realmente vá até o banco, e não apenas leia de um cache interno em memória que mascararia um problema na consulta de verdade.
Testar todo método de um DAO nem sempre compensa — métodos triviais (uma
atualização simples, delegada inteiramente ao ORM) tendem a ter pouco risco de bug;
métodos com lógica de consulta mais complexa (múltiplos filtros, junções) se beneficiam
bem mais de um teste dedicado. A mesma lógica de organização de testes de unidade
(@Before/@After para não repetir a abertura/fechamento do recurso a cada teste) se
aplica aqui.
Testes de sistema¶
Definição: Teste de sistema (end-to-end)
Teste que exercita a aplicação inteira do ponto de vista do usuário final — abrindo de fato um navegador, clicando em links, preenchendo formulários — sem conhecer nenhum detalhe interno (é uma caixa-preta). Diferente do teste de unidade (uma classe isolada) ou de integração (uma peça de infraestrutura isolada), o teste de sistema é o único que garante que o sistema funciona com tudo ligado ao mesmo tempo: interface, regras de negócio e banco de dados juntos.
Definição: Teste de sistema x teste de aceitação
A diferença é sutil e as duas coisas costumam se sobrepor: teste de sistema testa o sistema como caixa-preta (o foco é técnico — "funciona de ponta a ponta?"); teste de aceitação é usado para confirmar que uma funcionalidade está pronta do ponto de vista de negócio ("é isso que o usuário/cliente pediu?"). Na prática, testes de aceitação costumam ser automatizados com as mesmas ferramentas de teste de sistema, por serem o jeito mais direto de simular o comportamento real do usuário.
Selenium¶
Selenium é o framework mais usado para automatizar testes de sistema em aplicações web — ele controla de fato um navegador (abre, navega, clica, digita), como um usuário faria manualmente.
WebDriver driver = new FirefoxDriver();
driver.get("http://localhost:8080/usuarios/new");
WebElement nome = driver.findElement(By.name("usuario.nome"));
nome.sendKeys("Ronaldo Luiz de Albuquerque");
nome.submit();
assertTrue(driver.getPageSource().contains("Ronaldo Luiz de Albuquerque"));
Definição: WebDriver / findElement / By (Selenium)
WebDriver representa o navegador controlado pelo teste (FirefoxDriver,
ChromeDriver, ...); driver.get(url) navega até uma página; findElement(By...)
localiza um elemento na página atual — por name, id, texto do link
(By.linkText(...)), entre outros seletores. Uma vez localizado, o WebElement
aceita ações como sendKeys(texto) (digitar) e click()/submit().
Page Objects¶
Um teste de sistema escrito com chamadas diretas ao Selenium (findElement,
sendKeys...) fica rapidamente verboso e repetitivo — o mesmo problema de duplicação já
visto em testes de unidade, agora aplicado a interações com a página.
Definição: Page Object
Padrão de projeto para testes de sistema: uma classe que representa uma página
(ou parte dela) da aplicação, escondendo os detalhes de como o Selenium interage com
ela por trás de métodos com nomes de negócio (usuarios.novo(),
usuarios.existeNaListagem(nome, email)). O teste passa a ler como uma descrição do
comportamento esperado, não como uma sequência de cliques.
class UsuariosPage {
private WebDriver driver;
public UsuariosPage(WebDriver driver) {
this.driver = driver;
}
public void visita() {
driver.get("localhost:8080/usuarios");
}
public void novo() {
driver.findElement(By.linkText("Novo Usuário")).click();
}
public boolean existeNaListagem(String nome, String email) {
return driver.getPageSource().contains(nome) &&
driver.getPageSource().contains(email);
}
}
@Test
public void deveAdicionarUmUsuario() {
usuarios.novo();
cadastra("Ronaldo Luiz de Albuquerque", "ronaldo2009@terra.com.br");
assertTrue(usuarios.existeNaListagem("Ronaldo Luiz de Albuquerque", "ronaldo2009@terra.com.br"));
}
O WebDriver é recebido pelo construtor do Page Object (não instanciado dentro dele) —
o mesmo raciocínio de injeção de dependência já visto no OCP/DIP, permitindo que
@Before/@After do JUnit cuidem de abrir e fechar um único WebDriver, compartilhado
entre várias classes de página do mesmo teste. Esse padrão também explica a razão prática
por trás do exemplo de herança para "DSLs de teste" já visto em Boas
Práticas — extrair o boilerplate do
Selenium para uma classe reaproveitável, seja por composição (Page Objects) ou por
herança, é o mesmo objetivo: deixar o teste legível como uma frase, escondendo os
detalhes de como o navegador é controlado por trás dela.
Selenium WebDriver na prática¶
Além do básico acima, estes são os recursos que aparecem em qualquer suíte de testes de interface.
Localizando elementos. By.id, By.name, By.className, By.tagName, By.linkText/partialLinkText, By.cssSelector e
By.xpath. Prefira, nessa ordem: id (único), name, CSS e só então XPath (mais frágil e lento). Uma boa
dica para a equipe: combinar com os desenvolvedores atributos estáveis para teste (id ou data-testid), em vez de depender de
classes de estilo. findElement lança exceção se não achar; findElements devolve uma lista (vazia se não houver).
Interações: sendKeys (digitar; clear() antes de reescrever), click, submit, getText, getAttribute, isDisplayed,
isEnabled, isSelected; Select (selectByVisibleText, selectByValue, selectByIndex) para listas suspensas;
checkboxes e radio buttons se marcam com click() depois de conferir isSelected(); Actions para ações complexas
(clique duplo, botão direito, arrastar e soltar, teclas especiais, combinações), terminando sempre em .perform().
Alertas, janelas e tabelas: alerts, confirm e prompt do navegador exigem driver.switchTo().alert() (accept, dismiss,
sendKeys); cada janela/aba tem um identificador (getWindowHandle, getWindowHandles) e é preciso trocar o foco
(switchTo().window(...)) para interagir com a nova; tabelas se navegam localizando linhas e células (tr/td) por
índice ou texto — evite posições fixas em tabelas dinâmicas.
Asserts. Confira o resultado com assertTrue, assertFalse e assertEquals do JUnit, mensagens claras e um conceito por
teste. Não confie em Thread.sleep para sincronizar.
Esperas: o principal motivo de teste instável¶
Páginas modernas carregam e atualizam de forma assíncrona. Se o teste procura o elemento antes dele existir, falha — mesmo que o sistema esteja correto (teste instável, ou flaky).
| Espera | O que faz | Quando usar |
|---|---|---|
| Implícita | Define um tempo máximo global para localizar qualquer elemento no DOM (driver.manage().timeouts().implicitlyWait(...)) |
Configure uma vez, com valor baixo (até ~10 s, combinado com a equipe) |
Explícita (WebDriverWait + ExpectedConditions) |
Espera uma condição específica (visível, clicável, texto presente, URL...) até um limite e continua assim que ela é verdadeira | A forma recomendada para elementos dinâmicos |
Fluente (FluentWait) |
Explícita com intervalo de verificação configurável e exceções ignoradas | Casos finos, como esperar por elementos intermitentes |
Thread.sleep |
Pausa fixa | Evite: ou desperdiça tempo (espera 10 s quando bastavam 0,3 s) ou é curta demais em um ambiente lento |
Não misture espera implícita com explícita de forma descuidada: os tempos se somam e ficam imprevisíveis.
Page Object e Page Factory¶
O Page Object (veja acima) isola os detalhes da tela. O Page Factory o torna mais enxuto: declara-se o elemento como atributo
com a anotação @FindBy (id, name, css, xpath...) e inicializa-se a página com PageFactory.initElements(driver, this),
sem escrever findElement por todo lado. Recursos relacionados: @FindBys (elemento que casa todos os seletores),
@FindAll (casa qualquer um) e @CacheLookup (guarda o elemento em cache — só para elementos que nunca são recriados
pela página). Nomes semânticos (caixaDePesquisa) tornam o teste legível, e uma mudança de id na tela se corrige em um lugar.
Navegadores, headless e fábrica de drivers¶
Cada navegador tem seu driver (ChromeDriver, FirefoxDriver, EdgeDriver); o binário do driver precisa ser compatível com a versão
do navegador (hoje o Selenium Manager resolve isso sozinho; os livros antigos configuravam System.setProperty). Modo headless roda o navegador
sem interface gráfica — mais rápido e ideal para integração contínua (--headless=new no Chrome; o antigo PhantomJS, citado nos livros, foi descontinuado).
Uma fábrica de WebDrivers (um método que devolve o driver certo a partir de um parâmetro ou propriedade) permite rodar a mesma suíte em vários navegadores.
Para execução em paralelo e em vários ambientes, use Selenium Grid ou serviços na nuvem.
Dados de teste e organização da suíte¶
- Massa de dados: em vez de valores fixos, gere dados aleatórios plausíveis (biblioteca Faker: nomes, e-mails, CPFs...) para evitar colisões entre execuções; mantenha os testes independentes entre si.
- Suítes e categorias (JUnit): agrupe por funcionalidade (login, cadastro) ou por tipo (fumaça, positivos, negativos) com
@Suite/@SelectClasses(JUnit 5) ou@Category(JUnit 4), e inclua ou exclua categorias na execução. Não dependa da ordem de execução. - Boas práticas: testes pequenos e determinísticos; Page Objects; esperas explícitas; dados criados e limpos pelo próprio teste; screenshots e logs ao falhar; poucos testes de interface (a base da pirâmide são os de unidade). Cypress e Playwright são alternativas modernas com esperas automáticas.
Testes de serviços web¶
Um serviço web REST responde a requisições HTTP simples, trafegando dados em XML ou
JSON — testá-lo manualmente significa montar a requisição à mão (via curl, Postman,
...) e conferir a resposta visualmente. Rest-Assured é o framework mais usado para
automatizar isso em Java, com uma API fluente que lê quase como uma frase.
import static com.jayway.restassured.RestAssured.*;
import static com.jayway.restassured.matcher.RestAssuredMatchers.*;
@Test
public void deveRetornarListaDeUsuarios() {
XmlPath path = get("/usuarios?_format=xml").andReturn().xmlPath();
Usuario usuario1 = path.getObject("list.usuario[0]", Usuario.class);
Usuario usuario2 = path.getObject("list.usuario[1]", Usuario.class);
assertEquals(new Usuario(1L, "Mauricio Aniche", "mauricio.aniche@caelum.com.br"), usuario1);
}
Definição: get() / XmlPath / JsonPath (Rest-Assured)
get(url) faz uma requisição HTTP GET; .andReturn().xmlPath() (ou .jsonPath())
trata a resposta como XML (ou JSON) navegável — getObject(caminho, Classe.class)
desserializa um trecho específico da resposta diretamente num objeto Java, sem
exigir parsing manual. header(nome, valor) adiciona um cabeçalho HTTP à
requisição (ex.: Accept: application/xml, para pedir explicitamente aquele
formato de resposta) e parameter(nome, valor) adiciona um parâmetro de
querystring.
Enviar dados (POST) segue o mesmo espírito fluente — configurar o corpo da requisição,
executar, e validar a resposta:
@Test
public void deveAdicionarUmUsuario() {
Usuario joao = new Usuario("Joao da Silva", "joao@dasilva.com");
XmlPath retorno =
given()
.header("Accept", "application/xml")
.contentType("application/xml")
.body(joao)
.expect()
.statusCode(200)
.when()
.post("/usuarios")
.andReturn()
.xmlPath();
Usuario resposta = retorno.getObject("usuario", Usuario.class);
assertEquals("Joao da Silva", resposta.getNome());
}
Definição: given() / expect() / when() (Rest-Assured)
Os três blocos que organizam uma requisição mais complexa, na ordem em que costumam
aparecer no código (embora só sejam executados quando o método HTTP é chamado):
given() monta o que será enviado (headers, contentType, corpo via body());
expect() declara o que se espera da resposta antes mesmo de configurá-la
(statusCode(200)); when() dispara a ação de fato (get(url)/post(url)).
contentType informa o formato do corpo enviado; body(objeto) serializa o
objeto Java automaticamente, de acordo com esse contentType.
Definição: Cuidado com efeitos colaterais em testes de serviços web
Um teste que faz POST/PUT/DELETE contra um serviço real modifica dados de
verdade — inserir um usuário novo, por exemplo, pode quebrar um teste diferente
que verifica a quantidade total de usuários cadastrados (agora com um a mais do que
esperado). É a mesma preocupação de limpeza de estado já vista em testes de
integração — testes que criam dados via serviço web devem também limpá-los depois
(idealmente usando um serviço de exclusão exposto pela própria aplicação), para não
contaminar a execução de outros testes.
Testes de contrato¶
Definição: Teste de contrato
Verifica que consumidor e provedor de uma API concordam sobre o formato das requisições e respostas (campos, tipos, códigos de status), sem precisar subir todos os serviços juntos. Evita que uma mudança "inocente" de um serviço quebre quem o consome.
- Consumer-Driven Contracts (CDC): o consumidor define o que espera (o contrato) e o provedor o executa como teste na própria pipeline. Ferramentas: Pact, Spring Cloud Contract.
- Contrato-primeiro com OpenAPI: o arquivo OpenAPI é a fonte da verdade; gera-se validação automática de respostas e stubs.
- Fluxo típico: consumidor escreve o teste → gera o contrato (pact file) → publica no broker de contratos → o provedor o verifica a cada build → só faz deploy se passar.
- Complementa os testes com WireMock (simulam o outro lado) e é mais barato que testes end-to-end entre muitos serviços.
Testes de desempenho¶
| Tipo | Pergunta que responde |
|---|---|
| Carga (load) | O sistema aguenta o volume esperado de usuários? |
| Estresse (stress) | Onde está o limite e como ele falha ao ser ultrapassado? |
| Spike (pico) | Como reage a um aumento súbito de tráfego? |
| Endurance/*soak* | Mantém-se estável por horas (vazamento de memória, de conexões)? |
| Escalabilidade | O desempenho cresce ao adicionar instâncias? |
- Métricas: latência (média, p95, p99 — percentis mostram a cauda que a média esconde), throughput (requisições/s), taxa de erros, uso de CPU/memória e saturação do pool de conexões.
- Ferramentas: JMeter, Gatling (scripts em código), k6, Locust.
- Boas práticas: ambiente parecido com produção, dados realistas, definir metas antes (ex.: p95 < 300 ms), aquecer a JVM, medir um gargalo por vez e rodar na pipeline para detectar regressões. Para micro-benchmarks em Java use o JMH.
Testes de segurança¶
| Técnica | O que faz |
|---|---|
| SAST (Static Application Security Testing) | Analisa o código-fonte sem executar (SonarQube, SpotBugs + FindSecBugs, CodeQL) |
| DAST (Dynamic) | Ataca a aplicação em execução (OWASP ZAP, Burp Suite) |
| SCA (Software Composition Analysis) | Procura CVEs nas dependências (OWASP Dependency-Check, Dependabot, Snyk) |
| Varredura de segredos | Detecta senhas/chaves versionadas (Gitleaks, TruffleHog) |
| Pentest | Teste de invasão manual conduzido por especialistas |
Use o OWASP Top 10 como checklist (injeção, autenticação quebrada, exposição de dados sensíveis, configuração incorreta, componentes vulneráveis...). Integre SAST, SCA e varredura de segredos à pipeline de CI (shift-left: encontrar o problema cedo), mantenha as dependências atualizadas e trate as falhas críticas como bloqueantes. Veja também Segurança.
O processo de teste: planejar, projetar, implementar, executar e avaliar¶
Testar bem não é "sentar e testar". Mesmo em projetos pequenos o processo tem cinco atividades:
| Atividade | O que acontece | Artefatos |
|---|---|---|
| 1. Planejar | Definir escopo (priorizar o que tem mais valor de negócio e risco), recursos (pessoas, ferramentas, ambientes), tempo e custo e estratégia (técnicas, níveis, tipos) | Plano de teste, cronograma de teste |
| 2. Projetar | Projetar os casos de teste, avaliar reúso, definir produtos de apoio (mocks, simuladores), modelo de carga, ambiente e massa de dados; revisar o projeto em conjunto | Modelo de teste (casos, dados, carga) |
| 3. Implementar | Criar os scripts manuais ou o código de teste automático, gerar os dados, montar o ambiente (o mais parecido com produção possível) e as suítes | Scripts, suítes de teste |
| 4. Executar | Rodar os testes, registrar os defeitos em uma ferramenta de bug tracker (com passos e dados para reproduzir) e fazer análise crítica dos defeitos reportados | Relatórios de execução, defeitos |
| 5. Avaliar | Consolidar: completude (seguiu o modelo?), cobertura (que linhas foram exercitadas?), resultados (qualidade do software) e processo (os testes foram bons?) | Relatório de avaliação de teste |
Priorize por risco
Não dá para testar tudo. Uma matriz simples multiplica probabilidade de falha por impacto se falhar (por exemplo, de 1 a 3 cada) e testa primeiro o que tiver o maior produto; a funcionalidade mais crítica ganha mais tipos de teste (unidade, integração, sistema, desempenho) e a menos crítica, só os básicos.
Dados de teste são o insumo de qualquer teste. Ao projetá-los, considere quatro atributos: profundidade (volume), largura (variedade), escopo (relevância) e arquitetura (estrutura física). Use um banco isolado do de produção e dados fictícios.
Ferramentas de apoio: gestão de casos e planos de teste (TestLink), bug tracker (Mantis, Jira), execução e CI (Jenkins, GitLab CI) e análise de código (SonarQube).
Testes ágeis¶
Quando o teste é uma fase tardia e separada, dois problemas aparecem: quem codifica não se engaja nele (e quem testa chega tarde) e o teste vira "caça a defeitos" no fim. Testes ágeis invertem isso: testa-se desde o início, de forma contínua, por todos, com o objetivo de prevenir defeitos e dar feedback rápido, assim como o manifesto ágil faz para o desenvolvimento. Sinais de que a equipe não testa de forma ágil: testes só no final, testadores isolados dos requisitos, equipe de qualidade como "polícia" e testes manuais repetidos em vez de automatizados.
O papel de quem testa se expande: participa do levantamento de requisitos, dos critérios de aceitação, da estimativa e do planejamento, e atua como ponte entre o cliente e a equipe.
Quadrantes dos testes ágeis¶
| Voltados à tecnologia (apoiam o time) | Voltados ao negócio | |
|---|---|---|
| Guiam o desenvolvimento | Q1: testes de unidade e de componente; qualidade interna (TDD) | Q2: testes de história/aceitação e exemplos executáveis (BDD, ATDD) |
| Criticam o produto | Q4: desempenho, segurança e confiabilidade (ferramentas) | Q3: testes exploratórios, de usabilidade e de aceitação pelo usuário |
Veja também a pirâmide de testes: muitos testes no nível de unidade, menos nos níveis superiores.
Modelos de teste: TDD, BDD e ATDD¶
| Modelo | Quem escreve e o quê | Ferramentas |
|---|---|---|
| TDD (Test-Driven Development) | Desenvolvimento: escreve o teste de unidade antes do código (vermelho → verde → refatorar) — TDD | JUnit, Mockito |
| BDD (Behavior-Driven Development) | Equipe e negócio descrevem o comportamento em linguagem natural (Dado... Quando... Então, sintaxe Gherkin), que vira teste automatizado e documentação viva | Cucumber, SpecFlow, Behave |
| ATDD (Acceptance Test-Driven Development) | Cliente, desenvolvedor e testador definem os testes de aceitação antes de implementar a história; eles passam a ser o critério de "pronto" | FitNesse, Robot Framework, Cucumber |
Funcionalidade: Cadastro de editora
Cenário: Cadastro com dados válidos
Dado que estou na tela de cadastro de editora
Quando informo o nome "Editora Exemplo" e o desconto de 10%
E clico em "Salvar"
Então vejo a mensagem "Editora salva com sucesso"
E a editora aparece na listagem
Validação automática de código¶
Ferramentas de análise estática examinam o código sem executá-lo: SonarQube (qualidade geral: bugs, vulnerabilidades, code smells, duplicação, cobertura, dívida técnica, com quality gates que bloqueiam a entrega), Checkstyle (convenções de codificação), SpotBugs (sucessor do FindBugs: padrões de bugs em Java) e as equivalentes de cada linguagem (ESLint, Pylint, Flake8, FxCop/StyleCop). Rode-as no pipeline de integração contínua, junto com a cobertura (JaCoCo etc.) e com um mínimo exigido. Parece rigor demais, mas problemas encontrados cedo são muito mais baratos de corrigir.