← tópicos

Específicos · Engenharia de Software

Testes: pirâmide, unitário, integração, TDD, BDD (Gherkin); verificação × validação

Todo teste se define pelo padrão contra o qual compara — especificação é verificação, necessidade do cliente é validação —, e quase todo item errado do tópico descreve um tipo de teste…

Altíssima86 itens no tópico

A ideia que organiza o assunto

Software não falha porque alguém digitou errado. Falha porque alguém entendeu errado — o requisito, a fronteira de um intervalo, o contrato entre dois módulos — e o erro atravessou meses até aparecer em produção, onde custa cem vezes mais para consertar. Teste é a disciplina que tenta antecipar esse encontro.

Cada teste se define por duas perguntas, e responder a elas resolve a maior parte dos itens sem precisar de mais nada.

A primeira é contra o que ele compara:

Um sistema pode passar em toda a verificação e ser inútil: implementa perfeitamente a especificação errada. É por isso que as duas existem.

A segunda pergunta é de onde vêm os casos de teste:

Dessas duas perguntas sai o método de prova deste tópico, e ele é mecânico: leia a descrição primeiro, decida que teste é aquele, e só depois olhe o nome que o item escreveu. O molde dominante da banca aqui é definir um tipo de teste com precisão e assinar outro — “em um teste de integração, cada unidade é testada separadamente” é a descrição perfeita do teste de unidade. Quem julga pelo nome cai; quem julga pela descrição não precisa decorar item nenhum.

Por que se usa (e o que custa)

O ganho óbvio é encontrar defeito cedo. O menos óbvio, e mais cobrado, é que teste é também projeto: escrever o caso antes obriga a decidir o que o componente faz, qual é sua interface e o que conta como sucesso, antes de haver código para defender. É daí que saem o TDD e o BDD.

O ganho tem dois preços. O primeiro é manutenção: suíte automatizada é código, e código apodrece. Quando a interface do usuário muda muito e rápido, automatizar sai mais caro do que testar à mão — automação compensa no que é estável e repetido, não no que ainda muda de forma toda semana. O segundo é que teste não prova ausência de defeito: ele revela presença. Cobertura total do domínio de entrada é inviável, e é por isso que existem técnicas de seleção — partição de equivalência, valor limite, diretrizes de erros comuns — em vez de força bruta.

Como funciona

Estático × dinâmico. O teste dinâmico executa o programa com entradas escolhidas e compara a saída com a esperada. O estático examina o artefato sem executá-lo: revisão por pares, inspeção, walkthrough, análise estática de código. O estático acha o que o dinâmico nunca acha — complexidade excessiva, código morto, violação de padrão — e acha mais cedo e mais barato, porque não precisa de ambiente montado. Revisão por pares, portanto, não exige execução.

Os níveis, de dentro para fora. Unidade exercita o menor elemento com comportamento próprio, método ou classe, isolado das demais partes. O nível de componente trata o componente já montado e olha suas interfaces. Integração combina componentes já testados e ataca os erros que só existem entre eles: parâmetro trocado, contrato mal entendido, suposição incompatível. Sistema testa o todo montado contra a especificação. Aceitação confronta o sistema com a necessidade real, com o cliente decidindo.

Para isolar uma unidade é preciso substituir suas dependências pelos dublês. Um stub devolve resposta pronta e apoia a verificação de estado; um mock carrega expectativas sobre quais chamadas deveriam ocorrer e apoia a verificação de comportamento. Nenhum é genérico: cada um é construído para o cenário daquele teste.

A pirâmide. Base larga de testes de unidade — baratos, rápidos, muitos; meio de integração/serviço, mais caros e em menor número; topo estreito de interface/ponta a ponta, lentos e frágeis. Inverter a proporção produz a figura que a literatura chama de cone de sorvete: suíte lenta, que quebra por qualquer motivo e não diz onde está o defeito. Existe uma segunda pirâmide que a banca já usou, a de teste de aplicações web, e ela descreve outra coisa: a ordem do processo, que começa pelo imediatamente visível ao usuário (conteúdo e interface), segue para navegação, projeto e componentes e só ao final chega a infraestrutura, configuração, desempenho e segurança — da esquerda para a direita e de cima para baixo.

As técnicas de caixa-preta. O particionamento de equivalência divide o domínio de entrada em classes cujos elementos o programa deve tratar de forma equivalente, e testa um representante de cada — classes válidas e inválidas. A análise do valor limite vai às bordas, onde o defeito mora: o valor no limite e os imediatamente vizinhos, com casos derivados tanto do domínio de entrada quanto do de saída. A caixa-branca deriva casos de caminhos, condições, laços e fluxo de dados, e é a única que enxerga o código.

TDD. Ciclo de três estados: vermelho (escreve-se o teste, que falha), verde (escreve-se o mínimo de código que o faz passar), refatorar (melhora-se a estrutura interna sem alterar o comportamento externo). A regra operacional é seca: só se escreve código novo quando um teste automatizado falha. A suíte acumulada é o que torna a refatoração segura.

BDD. O TDD movido para a linguagem do negócio. Em vez de identificadores técnicos, nomes de teste que são frases com significado real, no registro das histórias de usuário; os cenários se escrevem em Gherkin, com as palavras-chave given, when, then. Quem fala a linguagem do domínio do negócio é o BDD; o TDD é escrito na linguagem de programação, pelo desenvolvedor.

Automação e JUnit. Os princípios FIRST governam o teste automatizado de unidade: fast, independent, repeatable, self-validating, timely. No JUnit os testes são independentes e não devem depender da ordem — mas a ferramenta permite fixá-la (@FixMethodOrder, @TestMethodOrder/@Order). @BeforeClass roda uma única vez antes de todos os testes da classe, e é onde se põe o que custa caro (abrir conexão, carregar massa). @EnabledOnOs restringe a execução aos sistemas operacionais listados. Testes parametrizados existem: dizer que o JUnit não aceita parâmetros é falso.

O que decide os itens

Verificação × validação — a distinção que decide mais itens que todas as outras somadas:

compara comperguntaquem faz
verificaçãoa especificaçãoconstruímos certo o produto?a equipe
validaçãoa necessidade do clienteconstruímos o produto certo?o usuário/cliente

Nível → foco → responsável:

nívelexercitaquem decide
unidademétodo, função, classe isoladadesenvolvedor
componenteinterfaces do componente montadodesenvolvedor
integraçãointerfaces entre módulos já testadosequipe de teste
sistemao sistema inteiro contra a especificaçãoequipe de teste
aceitaçãoo sistema contra a necessidade realcliente/usuário

Nome do teste → objetivo. É aqui que a banca mais troca etiquetas:

nomeobjetivo
regressãoa alteração quebrou o que já funcionava? (causa: mudança)
fumaça (smoke)o build integrado sobe e as funções principais rodam? rápido e frequente
cargacomportamento sob demanda esperada
estressecondições anormais — recursos em quantidade, frequência ou volume acima do previsto, até a ruptura
desempenhotempo de resposta e vazão sob carga definida
lançamento (release)libera a versão — inclui regressão obrigatoriamente

Caixa-preta × caixa-branca:

fonte dos casostécnicasno pentest
caixa-pretaespecificação, comportamento externopartição de equivalência, valor limitesem informação prévia
caixa-brancaestrutura interna, código-fontecobertura de comando, decisão, condição, caminhocom topologia, código e credenciais

TDD × BDD × refatoração:

o que é
TDDteste antes do código, na linguagem de programação; vermelho, verde, refatorar
BDDcenários na linguagem do negócio, em Gherkin: given, when, then
refatoraçãomelhorar a estrutura interna sem alterar o comportamento externo — é a 3ª fase do TDD, não o TDD

Stub × mock — stub verifica estado, mock verifica comportamento.

Qualidade interna × externa — unidade informa a interna (projeto do código, acoplamento); aceitação informa a externa (o que o usuário percebe).

Bottom-up × top-down na integração — bottom-up parte da infraestrutura e precisa de drivers; top-down parte do topo e precisa de stubs.

Estático × dinâmico — revisão, inspeção e análise estática não executam; unidade, integração, sistema e aceitação executam.

Números que caem

princípios FIRST5: fast, independent, repeatable, self-validating, timely
estados do ciclo TDD3: vermelho, verde, refatorar
blocos do cenário Gherkin3: given, when, then
partições de um intervalo fechado [a, b]3: abaixo de a, dentro, acima de b
casos mínimos para cobrir essas partições3, desde que um em cada faixa (um abaixo, um dentro, um acima)
fronteiras de n faixas contíguasn − 1
casos mínimos de valor limite por fronteira2 (o valor no limite e o imediatamente fora)
logo, 3 faixas contíguas em valor limiteno mínimo 4 casos, não 3
@BeforeClassexecuta 1 vez por classe; @Before, 1 vez por método de teste

Como a CEBRASPE derruba você aqui

Inversão — mais de um terço dos itens errados, e o tópico é feito de pares. A banca gira metade de um par e deixa o resto intacto. Antes × depois: no TDD, a especificação de teste escrita “logo após o código”. TDD × BDD: no TDD “utiliza-se uma linguagem derivada do domínio do negócio para a definição dos casos de testes” — essa é a linguagem do BDD. Stub × mock: “os stubs, diferentemente dos mocks, são mais apropriados para a verificação do comportamento da aplicação” — stub devolve estado, mock verifica comportamento. Unidade × aceitação: “os testes de unidade propiciam a qualidade externa” — unidade informa qualidade interna. Sentido do fluxo: um teste de aplicação web com “um fluxo de processo da direita para a esquerda e adota uma estratégia bottom-up”, quando se começa pelo que o usuário vê. E a propriedade definidora negada: o JUnit que “não permite que se interfira na ordem de execução de métodos de teste”; “a impossibilidade de definição de parâmetros para construtores e métodos”, quando testes parametrizados existem; @EnabledOnOs que “permite que testes sejam executados em qualquer sistema operacional”, quando ele restringe; testes que “mantêm uma relação de dependência entre si”; o singleton que “facilita o teste unitário”, quando compromete o isolamento; o teste de lançamento “sem a necessidade de reexecução de testes em funcionalidades já existentes”, quando regressão é exatamente isso. Monte o par correto na cabeça antes de julgar; não deixe o item montá-lo para você.

Definição certa, nome errado — quase um terço. A frase descreve com precisão um tipo de teste e assina outro. “Testes de aceitação buscam garantir que o sistema opere com cargas de trabalho variáveis” (é estresse); testes de regressão que servem a “verificar se o sistema pode operar na carga necessária” (é estresse); testes de integração que “buscam assegurar que o sistema opere com a carga necessária” (é estresse de novo); “Em um teste de integração”, quando o item descreve cada unidade avaliada isoladamente (é unidade); “Os testes de integração” que verificam conformidade com os requisitos do cliente (é aceitação); a “verificação de aceitação do servidor de aplicação” para o que é teste de unidade. Atalho que funciona quase sempre: a palavra carga elimina unidade, integração e aceitação como resposta. O mesmo molde atinge outros pares: “O TDD (test-driven development)” recebendo a definição de refatoração; refatorar descrito como “a simulação do comportamento dos componentes que interagem com a unidade de teste que está falhando”, que é dublê de teste; “No teste de penetração de caixa branca não são fornecidas informações prévias à equipe de testadores”, que é caixa-preta; e o FIRST recebendo um sexto princípio inventado, “flexíveis na aplicação em diferentes contextos”.

Fase, ator ou processo errado — um em cada sete. A ação é real, o dono ou o momento não. “Na etapa de projeto do ciclo de desenvolvimento de um software” realiza-se o aceite — o aceite fecha o ciclo. A equipe de desenvolvimento é quem “valida o uso do software e o libera para utilização pelo usuário final” — a aceitação é validação e cabe ao cliente. A revisão por pares apresentada como algo “que exige que o programa seja colocado em condições de execução próximas ao ambiente real de operação” — revisão é teste estático e não executa o programa. Vale também para nível de maturidade: “No nível F do MPS/BR”, quando verificação e validação são processos do nível D.

Absolutos. “Apenas as partições válidas.” Mocks como “objetos genéricos que atendem a todas as necessidades de testes”. No TDD, “é necessário incluir nos métodos elementos que possibilitem a captura dos dados durante o processo de testes” — o TDD não instrumenta código de produção. Achado o apenas, todas, impossível ou necessário, procure o caso que a palavra exclui.

Números. Só dois itens, e os dois na mesma ideia: oferecer valores que não cobrem o que a técnica exige. “21, 48 e 75” como conjunto mínimo para cobrir todas as partições de entrada de idades válidas entre 21 e 75 — falta um valor inválido de cada lado. E “três casos de teste” para 100% de cobertura de análise de valor-limite sobre três faixas — valor-limite conta fronteiras, não faixas.

Causa inventada. “Os testes no JUnit são realizados em sequência, por isso mantêm dependência entre si.” Ou, fora do laboratório, a solução “com efeitos mais imediatos será a contratação, com urgência, de mais programadores” para um projeto atrasado — a Lei de Brooks diz o contrário. A oração explicativa soa técnica e carrega a mentira; item que explique um porquê merece segunda leitura.

Erros clássicos

Achar que verificação e validação são sinônimos. Verificação olha a especificação; validação olha a necessidade. O processo de V&V precisa demonstrar as duas coisas — que o sistema atende à sua especificação e que seu comportamento sustenta os requisitos do cliente.

Trocar qual nível informa qual qualidade. Unidade dá retorno sobre a qualidade interna (projeto dos módulos, acoplamento, custo de manutenção); aceitação, sobre a externa. É contraintuitivo e por isso é cobrado.

Esquecer as classes inválidas. Particionamento sem entrada inválida testa metade do programa, justamente a metade em que o tratamento de erro mora.

Tratar caixa-preta como “teste de função” e nada mais. Ela também é o caminho para requisitos não funcionais — desempenho, valores-limite, compatibilidade —, porque avalia o sistema pelo lado de fora, que é onde esses requisitos se manifestam.

Achar que o Singleton facilita o teste. Instância única é estado global compartilhado entre testes: quebra o isolamento, impede substituir a dependência por dublê e derruba o I de FIRST.

Supor que TDD exige instrumentar o código de produção. Não exige. O teste exercita a interface pública; incluir nos métodos elementos para “capturar dados durante o teste” contamina o código com responsabilidade que não é dele.

Tratar teste de lançamento como “só o que é novo”. Toda versão nova reexecuta regressão sobre o que já existia — é aí que moram os defeitos introduzidos pela mudança.

Confiar que herança herda o teste. Método definido na superclasse executa em contexto diferente em cada subclasse, e precisa ser reexecutado em cada uma.

Praticar83 itens