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
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”.
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.
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.
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.
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.