Como fazer o desenvolvimento orientado a testes

15

Tenho apenas mais de 2 anos de experiência em desenvolvimento de aplicativos. Naqueles dois anos, minha abordagem para o desenvolvimento foi a seguinte

  1. Analisar requisitos
  2. Objeto / objetos principais de identidade, funções necessárias, comportamento, processo e suas restrições
  3. Criar classes, relação entre elas, restrições no comportamento e estados dos objetos
  4. Criar funções, processar com restrições comportamentais conforme os requisitos
  5. Teste manualmente o aplicativo
  6. Se as alterações dos requisitos modificarem componentes / funções, teste manualmente o aplicativo

Recentemente, fui apresentado ao TDD e sinto que essa é uma maneira muito boa de desenvolver o desenvolvimento, pois o código desenvolvido tem fortes motivos para existir e muitos problemas pós-implantação são atenuados.

Mas meu problema é que não sou capaz de criar testes primeiro. Em vez disso, estou identificando componentes e apenas escrevendo testes para eles antes de realmente escrever componentes. minha pergunta é

  1. Estou fazendo certo? Se não, o que exatamente eu tenho que mudar
  2. Existe alguma maneira de identificar se o teste que você escreveu é suficiente?
  3. É uma boa prática escrever teste para uma funcionalidade muito simples, que pode ser equivalente a 1 + 1 = 2 ou é apenas um exagero?
  4. É bom alterar a funcionalidade e testar adequadamente se o requisito muda?
Yogesh
fonte
2
"Estou identificando componentes e apenas escrevendo teste para eles antes de realmente escrever componentes.": Acho isso correto: você primeiro identifica a arquitetura grossa do seu sistema e depois começa a codificar. Durante a codificação (TDD), você trabalha os detalhes dos componentes individuais e, possivelmente, descobre problemas com sua arquitetura que podem ser corrigidos ao longo do caminho. Mas acho que você não começa a codificar sem nenhuma análise prévia.
Giorgio
Você também pode fazer testes de unidade / integração automatizados sem fazer TDD. Os dois costumam ficar confusos, mas não são a mesma coisa.
Andrés F.

Respostas:

19

Estou fazendo certo? Se não, o que exatamente eu tenho que mudar

É difícil dizer apenas a partir dessa breve descrição, mas suspeito que não, você não está fazendo certo. Nota: Não estou dizendo que o que você está fazendo não funciona ou é de alguma forma ruim, mas você não está fazendo TDD. O meio "D" significa "Dirigido", os testes conduzem tudo, o processo de desenvolvimento, o código, o design, a arquitetura, tudo .

Os testes informam o que escrever, quando escrever, o que escrever a seguir, quando parar de escrever. Eles dizem o design e a arquitetura. (O design e a arquitetura emergem do código por meio da refatoração.) O TDD não se refere a testes. Nem se trata de escrever testes primeiro: o TDD é permitir que os testes o conduzam, escrevê-los primeiro é apenas um pré-requisito necessário para isso.

Não importa se você realmente escreve o código ou o detalha completamente: você está escrevendo (esqueletos de) código em sua cabeça e depois escrevendo testes para esse código. Isso não é TDD.

Abandonar esse hábito é difícil . Muito, muito difícil. Parece ser especialmente difícil para programadores experientes.

Keith Braithwaite criou um exercício que ele chama de TDD como se você quisesse . Ele consiste em um conjunto de regras (baseadas nas Três Regras do TDD do tio Bob Martin , mas muito mais rigorosas) que você deve seguir rigorosamente e que foram projetadas para orientá-lo a aplicar o TDD com mais rigor. Funciona melhor com a programação de pares (para que seu par possa garantir que você não está infringindo as regras) e com um instrutor.

As regras são:

  1. Escreva exatamente um novo teste, o menor teste possível, que parece apontar na direção de uma solução
  2. Veja isso falhar; falhas de compilação contam como falhas
  3. Faça o teste de (1) passar escrevendo o menor código de implementação possível no método de teste .
  4. Refatorar para remover a duplicação e, se necessário, para melhorar o design. Seja rigoroso sobre o uso desses movimentos:
    1. você deseja um novo método - aguarde até o tempo de refatoração e, em seguida, crie novos métodos (sem teste) executando um destes procedimentos, e de nenhuma outra maneira:
      • preferido: extraia o método no código de implementação criado como (3) para criar um novo método na classe de teste ou
      • se você deve: mover o código de implementação de acordo com (3) para um método de implementação existente
    2. você deseja uma nova classe - espere até o tempo de refatoração e, em seguida, crie classes que não são de teste para fornecer um destino para um método Move e por nenhum outro motivo
    3. preencha classes de implementação com métodos executando o método Move e de nenhuma outra maneira

Normalmente, isso levará a designs muito diferentes do que o "método pseudo-TDD" comumente praticado de "imaginar em sua cabeça qual deveria ser o design e, em seguida, escrever testes para forçar esse design, implementar o design que você já havia previsto antes de escrever seu testes ".

Quando um grupo de pessoas implementa algo como um jogo do tic tac toe usando pseudo-TDD, eles geralmente terminam com projetos muito semelhantes, envolvendo algum tipo de Boardclasse com uma matriz 3 x 3 de Integers. E pelo menos uma parte dos programadores realmente escreveu essa classe sem testes, porque "sabem que vão precisar" ou "precisam de algo para escrever seus testes". No entanto, quando você força esse mesmo grupo a aplicar o TDD como se você quisesse, eles geralmente acabam com uma grande diversidade de designs muito diferentes, geralmente sem empregar nada remotamente semelhante a um Board.

Existe alguma maneira de identificar se o teste que você escreveu é suficiente?

Quando eles cobrem todos os requisitos de negócios. Os testes são uma codificação dos requisitos do sistema.

É uma boa prática escrever teste para uma funcionalidade muito simples, que pode ser equivalente a 1 + 1 = 2 ou é apenas um exagero?

Novamente, você tem o inverso: você não escreve testes de funcionalidade. Você escreve funcionalidade para testes. Se a funcionalidade para passar no teste for trivial, isso é ótimo! Você acabou de cumprir um requisito de sistema e nem sequer trabalhou duro para isso!

É bom alterar a funcionalidade e testar adequadamente se o requisito muda?

Não. O contrário. Se um requisito for alterado, você modifica o teste que corresponde a esse requisito, observa-o falhar e altera o código para fazê-lo passar. Os testes sempre vêm em primeiro lugar.

É difícil fazer isso. Você precisa de dezenas, talvez centenas de horas de prática deliberada , a fim de criar algum tipo de "memória muscular" para chegar a um ponto em que, quando o prazo se aproxima e você está sob pressão, nem precisa pensar nisso. , e fazer isso se torna a maneira mais rápida e natural de trabalhar.

Jörg W Mittag
fonte
1
Uma resposta muito clara, de fato! Do ponto de vista prático, uma estrutura de teste flexível e poderosa é muito agradável ao praticar TDD. Embora independente do TDD, a capacidade de executar testes automaticamente é inestimável para depurar um aplicativo. Para iniciar o TDD, os programas não interarctivos (estilo UNIX) são provavelmente os mais fáceis, porque um caso de uso pode ser testado comparando o status de saída e a saída do programa com o esperado. Um exemplo concreto dessa abordagem pode ser encontrado na minha biblioteca Gasoline for OCaml.
Michael Le Barbier Grünewald
4
Você diz "quando você força esse mesmo grupo a aplicar o TDD como se quisesse, eles geralmente acabam com uma grande diversidade de designs muito diferentes, geralmente sem empregar nada remotamente semelhante a um Conselho" como se fosse uma coisa boa . Não está claro para mim que é uma coisa boa e pode até ser ruim do ponto de vista de manutenção, pois parece que a implementação seria muito contra-intuitiva para alguém novo. Você poderia explicar por que essa diversidade de implementação é uma coisa boa ou pelo menos não é ruim?
Jim Clay
3
+1 A resposta é boa, pois descreve corretamente o TDD. No entanto, também mostra por que o TDD é uma metodologia defeituosa: é necessário um pensamento cuidadoso e um design explícito, especialmente quando confrontados com problemas algorítmicos. Fazer o TDD "às cegas" (como prescreve o TDD) fingindo não ter nenhum conhecimento de domínio leva a dificuldades e becos sem saída. Veja o infame desastre do solucionador de Sudoku (versão curta: TDD não pode superar o conhecimento do domínio).
Andrés F.
1
@AndresF .: Na verdade, o blog ao qual você vinculou parece ecoar as experiências que Keith fez ao fazer TDD como se você quisesse dizer: ao fazer "pseudo-TDD" para o Tic-Tac-Toe, eles começam criando uma Boardclasse com um Matriz 3x3 de ints (ou algo parecido). Considerando que, se você forçá-los a fazer TDDAIYMI, eles geralmente acabam criando um mini-DSL para capturar o conhecimento do domínio. Isso é apenas anedótico, é claro. Um estudo estatisticamente e cientificamente correto seria bom, mas, como é comum em estudos como esse, eles são muito pequenos ou muito caros.
Jörg W Mittag
@ JörgWMittag Me corrija se eu entendi mal você, mas você está dizendo que Ron Jeffries estava fazendo "pseudo-TDD"? Isso não é uma forma da falácia do "não verdadeiro escocês"? (Concordo com você sobre a necessidade de mais estudos científicos; o blog ao qual vinculei é apenas uma anedota colorida sobre o fracasso espetacular de uma instância específica do uso do TDD. Infelizmente, parece que os evangelistas do TDD são muito altos para o resto para que tenhamos uma análise real desse método e de seus alegados benefícios).
Andres F.
5

Você descreve sua abordagem de desenvolvimento como um processo "de cima para baixo" - você começa a partir de um nível de abstração mais alto e entra cada vez mais nos detalhes. TDD, pelo menos na forma como é popular, é uma técnica "de baixo para cima". E para alguém que trabalha principalmente de cima para baixo, pode ser realmente muito incomum trabalhar de baixo para cima.

Então, como você pode trazer mais "TDD" ao seu processo de desenvolvimento? Primeiro, suponho que seu processo de desenvolvimento real nem sempre seja "de cima para baixo", como você descreveu acima. Após a etapa 2, você provavelmente terá identificado alguns componentes que são independentes de outros componentes. Às vezes, você decide implementar esses componentes primeiro. Os detalhes da API pública desses componentes provavelmente não seguem apenas seus requisitos, os detalhes também seguem suas decisões de design. Este é o ponto em que você pode começar com o TDD: imagine como você usará o componente e como você realmente usará a API. E quando você começa a codificar esse uso da API na forma de teste, você começou com o TDD.

Segundo, você pode executar o TDD mesmo quando codificar mais de cima para baixo, começando com componentes que dependem primeiro de outros componentes não existentes. O que você precisa aprender é como "zombar" dessas outras dependências primeiro. Isso permitirá que você crie e teste componentes de alto nível antes de ir para os componentes de nível inferior. Um exemplo muito detalhado de como fazer TDD de maneira descendente pode ser encontrado neste post de Ralf Westphal .

Doc Brown
fonte
3

Estou fazendo certo? Se não, o que exatamente eu tenho que mudar

Você está indo muito bem.

Existe alguma maneira de identificar se o teste que você escreveu é suficiente?

Sim, use uma ferramenta de cobertura de teste / código . Martin Fowler oferece alguns bons conselhos sobre a cobertura dos testes.

É uma boa prática escrever teste para uma funcionalidade muito simples, que pode ser equivalente a 1 + 1 = 2 ou é apenas um exagero?

Em geral, qualquer função, método, componente etc. que você espere obter algum resultado, dadas algumas entradas, é um bom candidato para um teste de unidade. No entanto, como na maioria das coisas na vida (engenharia), você precisa considerar suas vantagens e desvantagens: O esforço é compensado ao escrever o teste de unidade, resultando em uma base de código mais estável a longo prazo? Em geral, opte por escrever o código de teste para a funcionalidade crucial / crítica primeiro. Posteriormente, se você encontrar erros associados a alguma parte não testada do código, adicione mais testes.

É bom alterar a funcionalidade e testar adequadamente se o requisito muda?

O bom de ter testes automatizados é que você verá imediatamente se uma alteração quebra as afirmações anteriores. Se você espera isso por causa de requisitos alterados, sim, não há problema em alterar o código de teste (na verdade, no TDD puro, você alteraria os testes primeiro de acordo com os requisitos e depois adotaria o código até que atendesse aos novos requisitos).

miraculixx
fonte
A cobertura do código pode não ser uma medida muito confiável. A imposição de% da cobertura geralmente resulta em muitos testes não necessários (como testes para todas as verificações nulas de parâmetros etc.) - que são testes para testes que agregam quase nenhum valor) e perda de tempo de desenvolvimento, embora seja difícil testar o código. caminhos podem não ser testados.
Paul
3

Escrever testes primeiro é uma abordagem completamente diferente para escrever software. Os testes não são apenas uma ferramenta de verificação adequada da funcionalidade do código (todos passam), mas a força que define o design. Embora a cobertura do teste possa ser uma métrica útil, ela não deve ser o objetivo em si - o objetivo do TDD não é obter uma boa% da cobertura do código, mas pensar na testabilidade do seu código antes de escrevê-lo.

Se você tiver problemas com a escrita de testes primeiro, eu recomendo fazer uma sessão de programação em pares com alguém com experiência em TDD, para que você tenha uma experiência prática sobre "a maneira de pensar" sobre toda a abordagem.

Outra coisa boa a se fazer é assistir a vídeos on-line nos quais o software está sendo desenvolvido usando TDD desde a primeira linha. Uma boa que eu costumava me apresentar ao TDD foi o Let's Play TDD de James Shore. Dê uma olhada, ele ilustrará como o design emergente funciona, que perguntas você deve se perguntar ao escrever testes e como novas classes e métodos são criados, refatorados e iterados.

Existe alguma maneira de identificar se o teste que você escreveu é suficiente?

Eu acredito que esta é a pergunta errada a ser feita. Quando você faz TDD, você escolhe o TDD e o design emergente como a maneira de escrever software. Se qualquer nova funcionalidade que você precisar adicionar sempre começar com um teste, ela sempre estará lá.

É uma boa prática escrever teste para uma funcionalidade muito simples, que pode ser equivalente a 1 + 1 = 2 ou é apenas um exagero?

Obviamente, depende, use seu julgamento. Prefiro não escrever testes nas verificações nulas dos parâmetros, se o método não fizer parte da API pública, mas, caso contrário, por que você não confirmaria que o método Add (a, b) retorna a + b, de fato?

É bom alterar a funcionalidade e testar adequadamente se o requisito muda?

Novamente, quando você altera ou adiciona nova funcionalidade ao seu código, você começa com um teste, seja adicionando novo teste ou alterando um existente quando os requisitos mudam.

Paulo
fonte