Skip to main content

Command Palette

Search for a command to run...

Teste de Software: O que é TDD?

Uma introdução prática com perguntas e respostas

Updated
10 min read

Tradução livre do post “Testing Software: What is TDD?” originalmente publicado por Eric Elliott em 13 de Janeiro de 2020.

~

O Desenvolvimento Orientado a Testes é um processo de desenvolvimento de software que segue os seguintes passos:

  1. Escreva um teste que falhe para um requisito;
  2. Implemente apenas código suficiente para fazer o teste passar;
  3. Refatore com confiança (se necessário).

TDD e o Método Científico

TDD é como o método científico, mas para software. O método científico é como aprendemos coisas sobre o mundo. Funciona assim:

  1. Faça uma observação;
  2. Faça uma pergunta sobre a observação;
  3. Forme uma hipótese ou explicação testável da observação;
  4. Faça uma previsão baseada na hipótese;
  5. Teste a previsão;
  6. Use os resultados para fazer novas hipóteses ou previsões.

Vamos dividir isso em elementos mais simples:

  1. Pergunta
  2. Previsão
  3. Experimento
  4. Tema

Com o TDD, em vez de aprender coisas sobre o mundo, estamos criando um novo mundo que deve estar de acordo com nossas especificações. Em vez de começar com uma observação sobre o mundo real, começamos com um requisito. TDD vira o processo de cabeça para baixo. Em vez de aprender sobre o mundo real, estamos ajustando o método científico para criar um novo método.

Método CientíficoTDD
PerguntaRequisito
PrevisãoExpectativa
ExperimentoAsserção
TemaImplementação

O fator chave tanto no método científico quanto no TDD é a previsão. Usando a previsão, eliminamos a possibilidade de uma ideia tendenciosa. Depois que o resultado já é conhecido, as pessoas muitas vezes acreditam que poderiam ter previsto o resultado: que elas sabiam o tempo todo. No código, o preconceito pode ter um efeito mais traiçoeiro: seu código pode produzir a saída errada, mas você pode acreditar que é a saída correta.

A ideia tendenciosa é um fenômeno comum com testes de snapshots, uma forma de teste que alerta quando algo muda, em vez de quando algo está errado. Se você acabou de fazer uma alteração no código e um teste de snapshot o alerta que houve uma alteração, você já está esperando o alerta e está inclinado a acreditar que o resultado está correto, o que o torna vulnerável a aceitar resultados incorretos.

Mas o efeito não se limita ao teste de snapshot. Muitas vezes, ao escrever testes após a implementação, os desenvolvedores tendem a aceitar a saída que a aplicação gera sem verificar cuidadosamente os resultados com cálculos manuais.

O TDD nos obriga a fazer um cálculo preditivo da saída esperada porque o código que produzirá a saída real ainda não existe. Não há oportunidade para o preconceito enviesar nosso julgamento.

Com isso em mente, podemos aprimorar nosso processo de TDD:

  1. Traduza um requisito em um teste que deve falhar;
  2. Crie uma expectativa com base no requisito;
  3. Execute o teste para obter a saída real. Deve falhar na primeira vez. Se passar, vá para o passo 1;
  4. Escreva a implementação. O teste deve passar desta vez. Se falhar, repita esta etapa;
  5. Vá para o próximo requisito e repita a partir da etapa 1.

Benefícios do TDD

  • Elimina o medo de alterações. Se uma alteração de código introduzir um bug, os desenvolvedores são alertados rapidamente, e o ciclo de feedback rápido do TDD os notificará quando for corrigido.

  • Uma maior segurança que torna o continuous deployment mais seguro. As falhas de teste interrompem o processo de deploy, permitindo que você corrija os bugs antes que os usuários finais tenham a chance de vê-los.

  • 40% — 80% menos bugs.

  • Melhor cobertura de código ao escrever testes antes da implementação. Como escrevemos código para fazer um teste específico passar, a cobertura do código será próxima de 100%.

  • Ciclo de feedback mais rápido durante o desenvolvimento. Sem o TDD, os desenvolvedores devem testar manualmente cada alteração para garantir que funcione. Com o TDD, os testes unitários podem ser executados automaticamente na mudança, fornecendo feedback mais rápido durante as sessões de desenvolvimento e debugger.

  • Ajuda no design da API: os desenvolvedores geralmente pensam na implementação do software antes de pensar na experiência do desenvolvedor ao usar o componente de software. O TDD inverte isso, forçando os desenvolvedores a projetar a API antes de trabalhar na implementação.

  • KISS e YAGNI“Keep it Simple, Stupid” e “You Ain't Gonna Need It” são dois princípios de design de software sobrepostos. KISS significa exatamente o que parece significar. Mantenha as coisas simples. YAGNI significa não desenvolver recursos e abstrações, a menos que esses recursos atendam a um requisito existente específico (não a um requisito futuro). O TDD ajuda nesse processo, forçando você a trabalhar em pequenas iterações, abordando um requisito de cada vez conforme necessário.

Custos do TDD

Os desenvolvedores sem experiência com TDD podem descobrir que são 15% a 30% mais lentos, mas com 1-2 anos de prática, o processo de feedback em tempo real do TDD pode aumentar a produtividade.

Dicas

  • A previsão vem antes da implementação. (Snapshots e test-after não se qualificam como TDD);
  • Use pequenas iterações. Teste e crie um requisito por vez;
  • Refatore com confiança, mas apenas quando for necessário;
  • Aprender TDD é um processo de aprendizagem da arquitetura modular de aplicações. Geralmente requer 1-2 anos de prática para ficar bom nisso. Seja paciente. TDD me ensinou a maior parte do que sei sobre arquitetura modular.

Perguntas e Respostas

A cobertura de código em 100% é algo bom?

Não é sensato almejar 100% de cobertura de código usando apenas testes unitários, porque existe muito código para integrar sua aplicação a outros serviços, como a rede, o disco, a tela do usuário e assim por diante. Os testes unitários funcionam melhor com funções determinísticas e puras: funções que, dadas as mesmas entradas, sempre retornam as mesmas saídas, sem nenhuma alteração em nenhum estado visível externamente.

Isso deixa uma parte do seu código descoberto por testes unitários. É aí que entram os testes funcionais e outros tipos de testes de integração. Os testes funcionais e de integração garantem que suas unidades de código funcionem entre si.

Quanto mais perto você chegar de 100% de cobertura com testes unitários, mais difícil será melhorar a cobertura dos testes unitários. Em algum momento, você se verá contornando obstáculos, o que torna seu código mais propenso a bugs e mais difícil de manter.

Selecionar o tipo correto de teste pode ajudá-lo a evitar a tentação de escrever mocks desnecessários. Também pode manter seu código simples, ajudando você a evitar redundâncias como injeção de dependência quando elas não são necessárias. Você não deve complicar seu código por causa dos testes unitários.

Regra geral: TDD deve tornar seu código mais simples, não mais complicado. Em resumo: 100% de cobertura é bom, mas você deve alcançá-la com uma combinação de testes unitários, de integração e funcionais. Use o tipo de teste para o cenário correto.


Você deve usar TDD para estilo visual?

Não. Em vez disso, as ferramentas de regressão visual e snapshot podem ajudá-lo a testar o estilo visual. Os designs visuais tendem a mudar com frequência e geralmente são mais subjetivos do que objetivos. É um desafio criar uma previsão científica de como algo deve parecer e como essa aparência pode ser alcançada com marcação e estilo. As ferramentas de regressão visual podem alertá-lo se a interface do usuário for alterada, mas um designer deve aprovar ou rejeitar a alteração, não um computador. Tenha em mente que os testes de regressão de estilo, de snapshot não se qualificam como TDD. Os snapshots visuais podem aprimorar seu processo, mas não podem substituir o TDD.


Você deve usar o TDD para questões de acessibilidade?

Sim! Preocupações como “estamos usando a marcação semântica correta?” e "o componente tem as dicas ARIA corretas?" podem ser facilmente testados unitariamente.


Às vezes, novos requisitos se sobrepõem a requisitos existentes que já passaram no teste. Como devo lidar com isso?

Se o código existente já cobre ou não um caso de teste, você ainda deseja adicionar explicitamente o novo caso de teste porque os testes ajudam a documentar os requisitos da API. Quando isso acontece, você pode comentar o código que faz o teste passar, escrever o novo teste, vê-lo falhar e descomentar o código para ver o teste passar novamente. É essencial ver todos os testes falharem porque se você nunca viu um teste falhar, você não sabe se o teste funciona.


Às vezes, a saída de uma função é difícil ou impossível de prever. Usando o processo de TDD, como prever o valor esperado?

Deve ser incomum encontrar situações em que você realmente não possa prever o resultado de uma chamada de função.

Muitas vezes é um code smell indicando que a função não é determinística. Por exemplo, a função usa a hora atual do sistema ou gera um número aleatório.

Crie parâmetros opcionais para fornecer os dados gerados pela função, como uma data ou um número aleatório.

import cuid from 'cuid';

const createTodo = ({
  description,
  date = Date.now(),
  id = cuid()
}) => ({
  type: 'createTodo',
  payload: { description, date }
});

Se você não passar date ou id, ele irá gerá-los na hora da chamada. Em seus testes unitários, você pode passá-los para obter uma saída determinística.

Às vezes, você não está familiarizado com o comportamento de uma API. Talvez não esteja bem documentado ou demore muito para encontrar a documentação. Nesses casos, geralmente começo com uma expectativa intencionalmente errada, vejo a saída real e, em seguida, copio e colo a saída na expectativa do teste.

Para testar o teste, comentarei o código de implementação e garantirei que o teste falhe sem ele, depois reverterei os comentários e observarei o teste passar novamente. Apenas tenha em mente que esse estilo de teste é mais fraco do que os testes orientados a previsão porque você está pulando a etapa crítica de previsão no processo científico e introduzindo a possibilidade de preconceitos.


Devo rastrear o processo de TDD no meu histórico do git?

Não. Uma boa cobertura de teste é garantia suficiente de que o processo de TDD está funcionando. Commits como “teste unitário para o recurso x” apenas atrapalharão seu histórico do git e tirarão você do fluxo de trabalho que você está fazendo no código.

À medida que você cria recursos, sua mente deve estar no código que está escrevendo, não no processo por causa do processo. Assim como você deve manter seu código simples, você também deve manter seu processo simples.

Por exemplo, você pode usar um script de observação que executa automaticamente seus testes em cada arquivo salvo. Você não precisa executar nenhuma etapa extra para ver se seu código funciona. Escreva o código de implementação e verifique a saída do teste em seu console de desenvolvimento, enquanto permanece no fluxo do código de construção. Você não precisa parar de escrever código, executar manualmente os testes e, em seguida, voltar a escrever o código novamente. Não se distraia do seu trabalho real.


Quando refatoramos nosso código, muitos testes começam a falhar. Devemos refatorar os testes ou escrever novos?

Essa pergunta expõe um uso indevido comum da palavra “refatorar”. Refatorar não significa “qualquer mudança de código”. Em vez disso, “refatorar” significa que estamos alterando a estrutura interna da unidade em teste sem alterar seu comportamento externo. Use black-box testing, significa que para cada unidade de código (independentemente da granularidade), você testa apenas a área de superfície da API dessa unidade.

Não escreva testes que conheçam os detalhes de implementação interna. Por exemplo, se você estiver testando um método map para uma nova estrutura de dados, não verifique a estrutura de dados em si, porque se você fizer isso e depois alterar a estrutura de dados, seu teste falhará, mesmo que o comportamento externo permaneça o mesmo.

Os testes não devem ser interrompidos quando você refatorar uma parte do seu código. Os testes unitários só devem ser interrompidos se os requisitos de comportamento da unidade mudarem. Por exemplo, você começa retornando um array e depois descobre que precisa retornar um objeto. Nessa fase, você tem uma escolha a fazer. Seria necessário mais trabalho para refatorar os testes existentes ou reescrevê-los?

Às vezes, os requisitos mudam tão profundamente que o código do teste existente não é muito utilizável. Nesses casos, você ainda deve examinar cuidadosamente os requisitos e decidir quais se aplicam ao novo comportamento e quais não. Anote todos os requisitos aplicáveis ​​que você precisa testar e, em seguida, descarte os testes desnecessários.