Pular para conteúdo

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 + sufixo Test (AvaliadorTest para testar Avaliador) — 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:

when(daoFalso.salva(leilao)).thenThrow(new RuntimeException());

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.

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.