Módulo 12 — Testes de Software: fundamentos e tipos

Onde estamos. Já sabemos especificar o que um sistema deve fazer e projetá-lo com cuidado. Agora enfrentamos uma pergunta desconfortável: como saber se o que construímos realmente funciona? Este módulo trata dos fundamentos de teste de software — o vocabulário, as abordagens e as técnicas que separam quem “roda o programa e vê se dá certo” de quem testa como engenheiro. Chegue à aula entendendo por que testar é intelectualmente exigente, e não uma formalidade no fim do projeto.

Abro com uma confissão que costuma incomodar quem começa: você nunca vai provar, por meio de testes, que um programa está correto. Por mais que teste, o máximo que consegue é aumentar sua confiança de que o software funciona nas situações examinadas. Essa limitação não é falha de método; é uma propriedade lógica da atividade. Testar bem não é rodar o programa muitas vezes — é escolher, com método, os poucos casos com mais chance de revelar um erro.

Verificação e validação: duas perguntas diferentes

Antes das técnicas, precisamos separar duas perguntas que o senso comum mistura. A verificação pergunta: “estamos construindo o produto corretamente?” — compara o software com a especificação, checando se o sistema faz o que foi documentado. A validação pergunta algo mais profundo: “estamos construindo o produto certo?” — compara o software com as necessidades reais de quem vai usá-lo, e admite a possibilidade dolorosa de a especificação estar toda correta e ainda assim resolver o problema errado.

Sommerville insiste nessa distinção porque as duas falhas têm causas e remédios diferentes. Um erro de verificação nasce dentro da construção: o programador entendeu o requisito, mas o traduziu mal em código. Um erro de validação nasce antes: todos fizeram seu trabalho, mas ninguém percebeu que o requisito não correspondia ao que o usuário precisava. Testes, no sentido estrito que estudaremos, são a principal ferramenta de verificação; a validação exige também revisões, protótipos e o usuário. É a esse par de frentes complementares que se refere quem fala em “V&V”.

ImportanteA ideia central que você deve carregar

Verificação confronta o software com a especificação; validação, com a necessidade real. Um sistema pode passar em toda a verificação e ainda falhar na validação, porque construímos com perfeição o produto errado. Testar é sobretudo verificação, e por isso jamais dispensa as outras formas de avaliação.

Engano, defeito e falha: três palavras que não são sinônimos

Esta é uma das confusões mais comuns e custosas de quem fala de qualidade sem rigor. No português cotidiano, “erro”, “bug” e “falha” viram sinônimos. Na engenharia, nomeiam três coisas distintas, em momentos distintos, e confundi-las impede você de raciocinar sobre a causa de um problema. Usarei os termos com precisão; faça o mesmo.

Um engano — ou erro humano — é uma ação equivocada de uma pessoa. O programador pensou “o índice vai de 1 a 10” quando ia de 0 a 9; a analista entendeu “maior que” onde se lia “maior ou igual”. O engano é mental, acontece na cabeça de quem constrói. Quando se materializa no código, produz um defeito — o bug. O defeito é estático: uma linha errada, uma condição invertida, um caso não tratado; está lá, mesmo que ninguém o execute. Já a falha é o que se observa quando o software, ao ser executado, se comporta de modo diferente do esperado por causa daquele defeito. A falha é dinâmica: só existe em execução, e só aparece quando o caminho que contém o defeito é percorrido com os dados que o expõem.

A distinção não é preciosismo. Na cadeia causal, o engano gera o defeito, e o defeito pode gerar a falha. Digo “pode” porque nem todo defeito produz falha observável — um num trecho nunca executado com os dados certos permanece adormecido. Por isso um programa pode conter dezenas de defeitos e passar em todos os testes, que não exercitaram as condições que os despertariam.

flowchart LR
    A[Engano<br/>erro humano] -->|se materializa em| B[Defeito<br/>no código]
    B -->|quando executado<br/>com dados certos| C[Falha<br/>em execução]
    B -.->|se nunca exercitado| D[Permanece<br/>adormecido]
    style A fill:#fff3cd,stroke:#664d03
    style B fill:#f8d7da,stroke:#842029
    style C fill:#d1e7dd,stroke:#0f5132
    style D fill:#e2e3e5,stroke:#41464b

Por que testar nunca prova a ausência de defeitos

Voltamos ao ponto que abre o módulo, com o vocabulário afinado. Há uma frase atribuída a Edsger Dijkstra que todo engenheiro deveria saber de cor: o teste pode revelar a presença de defeitos, mas nunca provar a sua ausência. Isso é logicamente inevitável.

Um programa minimamente interessante tem um espaço de entradas praticamente infinito: dois inteiros já dão bilhões de combinações; texto, infinitas. Testar exaustivamente é impraticável, então examinamos sempre uma amostra minúscula dos comportamentos possíveis. Se um teste revela uma falha, você aprendeu algo definitivo: existe um defeito, e você tem a prova. Mas se todos passam, você não provou nada sobre os casos que não experimentou. Ausência de evidência não é evidência de ausência: o defeito adormecido continua lá, no que sua amostra não tocou.

NotaA consequência prática da tese de Dijkstra

Como não podemos provar corretude por teste, o objetivo deixa de ser “mostrar que funciona” e passa a ser “tentar quebrar o programa da forma mais eficiente possível”. O bom testador é adversarial: projeta casos para expor falhas, pois cada falha encontrada é um defeito a menos para o usuário. Uma suíte que nunca falha pode significar código excelente — ou testes fracos. O amador testa esperando ver verde; o profissional testa caçando o vermelho.

Os níveis de teste

Testar um sistema inteiro de uma vez seria como inspecionar um prédio só pela fachada. Por isso organizamos o teste em níveis, cada um com alvo e propósito diferentes. Pressman e Sommerville apresentam variações da mesma escada; vamos subi-la degrau a degrau.

No nível mais baixo está o teste de unidade. Isolamos a menor peça testável — em geral uma função ou método — e verificamos que ela, sozinha, faz o que promete. É o mais barato, o mais rápido de rodar e o que aponta com mais precisão onde está o defeito. Subindo um degrau, o teste de integração verifica se as unidades já testadas conversam entre si: cada método pode estar perfeito e a combinação falhar porque um esperava a data num formato e o outro entregava em outro — defeitos de interface que nenhum teste de unidade veria.

Mais acima, o teste de sistema avalia o software completo e integrado do ponto de vista técnico: os fluxos ponta a ponta funcionam? Os requisitos não funcionais — desempenho, segurança, capacidade — são atendidos? E, no topo, o teste de aceitação muda de perspectiva: quem julga é o cliente ou o usuário, e a pergunta não é mais “está tecnicamente correto?”, mas “isto atende à minha necessidade?”. A aceitação é a validação de que falamos no início: os níveis inferiores fazem verificação; o topo faz validação.

Nível Alvo do teste Pergunta que responde Quem costuma conduzir
Unidade Uma função ou método isolado Esta peça faz o que promete? Desenvolvedor
Integração A colaboração entre unidades As peças conversam corretamente? Desenvolvedor
Sistema O software completo integrado O sistema atende aos requisitos? Equipe de teste
Aceitação O sistema sob o olhar do usuário Isto resolve a minha necessidade? Cliente / usuário

O diagrama a seguir relaciona esses níveis às atividades de desenvolvimento, na lógica do modelo em V. A ideia a reter é a simetria: cada atividade de construção, no ramo descendente, tem um nível de teste que a verifica, no ascendente. A aceitação confere os requisitos; o sistema, o projeto de alto nível; a integração, o projeto dos componentes; a unidade, a implementação.

flowchart TB
    R[Requisitos] -.verificado por.-> AC[Teste de Aceitação]
    AR[Projeto de Arquitetura] -.verificado por.-> ST[Teste de Sistema]
    CO[Projeto de Componentes] -.verificado por.-> IT[Teste de Integração]
    IM[Implementação] -.verificado por.-> UT[Teste de Unidade]
    R --> AR --> CO --> IM
    UT --> IT --> ST --> AC
    style R fill:#cfe2ff,stroke:#084298
    style AR fill:#cfe2ff,stroke:#084298
    style CO fill:#cfe2ff,stroke:#084298
    style IM fill:#cfe2ff,stroke:#084298
    style UT fill:#d1e7dd,stroke:#0f5132
    style IT fill:#d1e7dd,stroke:#0f5132
    style ST fill:#d1e7dd,stroke:#0f5132
    style AC fill:#fff3cd,stroke:#664d03

Essa organização tem uma economia embutida. Testes de unidade são rápidos e baratos, então convém ter muitos; os de sistema são lentos e caros de montar, então convém ter poucos, porém bem escolhidos. Essa proporção é o que a literatura chama de pirâmide de testes.

flowchart TB
    A["Aceitação / ponta a ponta<br/>(poucos, lentos, caros)"]
    B["Integração<br/>(quantidade intermediária)"]
    C["Unidade<br/>(muitos, rápidos, baratos)"]
    A --> B --> C
    style A fill:#f8d7da,stroke:#842029
    style B fill:#fff3cd,stroke:#664d03
    style C fill:#d1e7dd,stroke:#0f5132,stroke-width:2px

A pirâmide comunica uma advertência. Quando uma equipe inverte a proporção — poucos testes de unidade e muitos testes lentos de ponta a ponta —, acaba com uma suíte frágil, demorada e difícil de manter: o antipadrão às vezes chamado de “cone de sorvete”. A base larga de testes de unidade sustenta a confiança sem sufocar o ritmo.

Caixa-preta e caixa-branca: duas formas de enxergar o alvo

Independentemente do nível, você projeta um teste sob duas óticas. Na caixa-preta, você ignora como o código é feito por dentro e testa apenas o comportamento observável: dada esta entrada, o programa deveria produzir esta saída? Você raciocina a partir da especificação, tratando o componente como uma caixa opaca. Na caixa-branca — ou estrutural —, você abre a caixa e usa o conhecimento do código para desenhar os testes: quais caminhos existem, quais condições precisam ser verdadeiras, quais linhas ainda não foram exercitadas.

As duas não competem, complementam-se. A caixa-preta encontra defeitos de “o que deveria fazer” — funcionalidades ausentes ou incorretas em relação ao requisito. A caixa-branca encontra defeitos de “como está feito” — um ramo do if nunca percorrido, um caso extremo que a lógica interna deixa escapar. Um testador maduro usa a caixa-preta para a especificação e a caixa-branca para o código, ciente de que nenhuma, sozinha, basta.

Técnicas de caixa-preta: um exemplo trabalhado

A grande questão da caixa-preta é: já que não posso testar todas as entradas, quais poucas escolho? Duas técnicas clássicas respondem a isso; vou trabalhá-las com um exemplo.

A primeira é o particionamento por classes de equivalência: dividir o espaço de entradas em grupos — as classes — dentro dos quais o programa deveria se comportar de maneira uniforme. Se ele trata todos os valores de um grupo do mesmo jeito, testar um representante é tão informativo quanto testar todos: em vez de milhões de casos, escolhe-se um por classe. E não esqueça as classes inválidas: valores fora do permitido também precisam de representante, e é aí que muito defeito se esconde.

Suponha uma função que valida a idade informada num cadastro, aceitando idades de 18 a 65 anos, inclusive. O espaço de entradas se divide em três classes de equivalência quanto ao valor, mais as classes inválidas.

Classe de equivalência Faixa Válida? Valor representante
Abaixo do mínimo idade < 18 Não 10
Dentro da faixa aceita 18 ≤ idade ≤ 65 Sim 40
Acima do máximo idade > 65 Não 80
Não numérica / ausente entrada inválida Não “abc”

A segunda técnica corrige um ponto cego da primeira. Defeitos raramente moram no meio das faixas; concentram-se nas fronteiras, onde o programador escreve as condições e erra — troca < por <=, esquece um caso, desloca um índice. A análise de valor-limite manda testar as bordas de cada classe válida: o último valor de fora, o primeiro de dentro, o último de dentro e o primeiro de fora. No exemplo, com limite inferior em 18 e superior em 65, os valores de fronteira que merecem caso próprio são estes.

Fronteira Valor Resultado esperado
Logo abaixo do mínimo 17 Rejeitar
No mínimo exato 18 Aceitar
Logo acima do mínimo 19 Aceitar
Logo abaixo do máximo 64 Aceitar
No máximo exato 65 Aceitar
Logo acima do máximo 66 Rejeitar

Note a economia: combinando as duas técnicas, você cobre o comportamento inteiro da função com uma dúzia de casos bem escolhidos — justamente os de maior probabilidade de flagrar um defeito — em vez de tentar o impossível de testar todas as idades.

Um teste de unidade em Dart

Vamos transformar a análise em código. Em Dart, escrevemos testes com o pacote da linguagem: test declara cada caso e expect afirma o resultado esperado. Considere a função sob teste.

/// Aceita idades de 18 a 65 anos, inclusive.
bool idadeValida(int idade) {
  return idade >= 18 && idade <= 65;
}

Agora exercitamos os valores-limite que a análise indicou. Cada test recebe uma descrição legível, e os casos não são aleatórios: são as fronteiras das classes.

import 'package:test/test.dart';

void main() {
  group('idadeValida — análise de valor-limite', () {
    test('rejeita logo abaixo do mínimo (17)', () {
      expect(idadeValida(17), isFalse);
    });

    test('aceita o mínimo exato (18)', () {
      expect(idadeValida(18), isTrue);
    });

    test('aceita logo acima do mínimo (19)', () {
      expect(idadeValida(19), isTrue);
    });

    test('aceita logo abaixo do máximo (64)', () {
      expect(idadeValida(64), isTrue);
    });

    test('aceita o máximo exato (65)', () {
      expect(idadeValida(65), isTrue);
    });

    test('rejeita logo acima do máximo (66)', () {
      expect(idadeValida(66), isFalse);
    });
  });
}

Imagine que um colega, por descuido, tivesse escrito idade < 65 em vez de idade <= 65. Todos os testes passariam, exceto o do valor 65 — o caso de fronteira que denunciaria o engano. Aí está, em miniatura, a filosofia do módulo: o defeito estava numa fronteira, e foi um teste de fronteira, projetado de propósito, que o revelou.

Cobertura em caixa-branca

Quando abrimos a caixa, surge uma pergunta: quanto do código meus testes realmente exercitaram? A resposta vem das métricas de cobertura. A mais simples é a de comandos: qual porcentagem das linhas executáveis foi percorrida por pelo menos um teste? Mais exigente é a de decisões ou ramos, que pergunta se cada desvio condicional foi tomado tanto pelo lado verdadeiro quanto pelo falso — pois uma linha if pode ser “coberta” só pelo ramo verdadeiro, deixando o falso jamais testado.

Cabe um alerta que Pressman sublinha: cobertura alta é necessária, mas não suficiente. Você pode atingir cem por cento de cobertura de comandos e ainda ter defeitos — executar uma linha não é o mesmo que verificar que ela produz o resultado certo, e a cobertura mede apenas o código que existe, jamais a funcionalidade que faltou. Um requisito esquecido não aparece em métrica alguma, pois não há linha para cobrir. Por isso a cobertura serve como detector de buracos — mostra o que você ainda não testou —, nunca como certificado de qualidade: use-a para descobrir ramos esquecidos, não para declarar vitória.

Teste de regressão

Falta um tipo de teste que atravessa todos os níveis e talvez seja o de maior valor no longo prazo. Lembra que, no primeiro módulo, dissemos que o software se deteriora por mudança? O teste de regressão é a defesa direta contra isso. Toda vez que você altera o código — para corrigir um defeito ou adicionar uma funcionalidade —, corre o risco de quebrar algo que antes funcionava: a regressão é o retorno de um comportamento já correto a um estado defeituoso.

Ele consiste em reexecutar a suíte existente após cada mudança, garantindo que o novo código não quebrou o antigo. É aqui que o investimento em testes automatizados de unidade se paga com juros: com centenas de testes rápidos, você modifica o sistema com coragem, pois se algo regredir a suíte acusa na hora e aponta onde. Sem essa rede, cada alteração vira uma aposta, e o medo de mexer no código inicia a paralisia que condena sistemas legados. A automação não é luxo: é o que torna a evolução segura.

Um teste rodado à mão protege o código naquele instante; um automatizado protege a cada nova mudança, sem custo adicional. É a diferença entre verificar uma vez e vigiar continuamente.

Como este módulo se conecta ao restante do curso

Neste módulo você ganhou o vocabulário e as técnicas fundamentais de teste: verificação e validação, a cadeia engano-defeito-falha, a limitação lógica que o teste jamais supera, os níveis, caixa-preta e caixa-branca, o particionamento e o valor-limite, a cobertura e o teste de regressão. Adiante, veremos o teste deixar de ser etapa final para guiar o próprio ato de programar, no desenvolvimento orientado a testes; mais à frente, esses conceitos se integram à discussão maior de qualidade de software.

Síntese

Retenha três ideias. A primeira: testar é adversarial e limitado por natureza — o teste revela a presença de defeitos, nunca prova a ausência, e por isso o bom testador projeta casos para quebrar o programa, não para vê-lo passar. A segunda: há método por trás dos bons testes — níveis para organizar o alvo, caixa-preta e caixa-branca para a ótica, particionamento e valor-limite para selecionar os poucos casos que mais importam. A terceira: o valor de um teste se multiplica quando automatizado, pois protege o sistema contra a regressão a cada mudança futura. Chegue à aula com essas três ideias assentadas.

Para consolidar antes da aula: pegue uma função simples que você já escreveu, com ao menos uma condição numérica, e identifique suas classes de equivalência e seus valores-limite. Depois pergunte-se, honestamente, quantos desses casos seus testes atuais cobrem. Se a resposta o surpreender, você entendeu o essencial do módulo.