TDD - é apenas sobre testes de unidade? [duplicado]

37

Eu entendo corretamente que o TDD clássico é apenas sobre testes de unidade? Não me entenda mal: eu sei a diferença entre o TDD e apenas o teste de unidade. Estou perguntando se é correto usar o teste de integração no fluxo de trabalho do TDD.

Atualmente trabalho no projeto em que o TDD é certamente apenas sobre testes de unidade e há pelo menos um problema sério com ele. A maioria dos nossos testes de unidade são testes comportamentais que muitas vezes se tornam falso-negativos (falso vermelho) durante a refatoração (apenas porque algumas seqüências de dependências foram alteradas). O projeto foi criado no estilo TDD para tornar a refatoração simples e, de repente, a refatoração tornou-se um inferno. Falha épica!

A decisão mais óbvia agora é tornar nosso teste não unitário, mas integrador (mas ainda com TDD). Então, se anteriormente nós depuramos / ridicularizamos as dependências de classe, agora não faremos isso (pelo menos nem todas). Como resultado, a maioria dos nossos testes se tornará estado (em vez de comportamental). E os testes de estado tornam-se falso-negativos muito mais raramente (porque eles testam o resultado, mas não o fluxo de trabalho da execução).

Então, eu gostaria de saber o quão difundida é uma abordagem do uso de TDD com testes de integração. Está tudo bem? Ficaria muito grato por quaisquer recursos sobre este tema. Li este artigo, mas é um pouco ... estranho

Atualizar. Aqui vou esclarecer o que quero dizer em teste de unidade e teste de integração. Teste de unidade é um teste que stubs / mocks todas as dependências de classe. O teste de integração tem implementações reais de dependências (embora possa fragmentar / ridicularizar algumas dependências, se necessário).

    
por SiberianGuy 09.08.2011 / 20:28
fonte

10 respostas

20

No TDD, vou testar as unidades juntas quando fizer sentido. Do jeito que eu vejo, eu uso mock / stubs por dois motivos: o comportamento do sistema se torna muito complexo para testar efetivamente com a implementação completa, e a implementação completa pode fazer coisas que eu não quero que aconteçam.

Basicamente, quanto mais objetos estiverem envolvidos em um teste, mais difícil será o sistema prever. Você tem a previsão do comportamento do sistema para escrever o teste. Alguns objetos podem ter um comportamento complexo e pode ser difícil induzir casos de borda específicos em uma unidade. Assim, pode ser realmente útil zombar desses objetos.

Em outros casos, os objetos têm efeitos colaterais indesejáveis. Suponha que você tenha um CreditCardProcessor. Você não quer realmente cobrar cartões de crédito enquanto seus testes estão sendo executados. Outros itens, como desenhar gráficos ou acessar recursos da web, podem estar na mesma categoria.

Quando um objeto tem uma dependência, como você decide incluir o objeto real ou algum tipo de simulação / stub?

Primeiramente, se houver alguma possibilidade de que o comportamento do objeto mude durante o desenvolvimento, eu o farei. Por exemplo, considere uma classe de fila de prioridade versus uma classe de estratégia de preço. Uma fila de prioridades quase sempre sempre manterá o mesmo comportamento. No entanto, sua estratégia de preços provavelmente mudará muito. Como resultado, você não quer que outros testes dependam do comportamento dentro da estratégia de preços. Se o fizerem, você acabará quebrando outros testes desnecessariamente. No entanto, não é realmente um grande problema para a fila de prioridades, porque o comportamento nunca deve mudar.

Em segundo lugar, como "gordura" é a interface entre os objetos? Se os objetos tiverem uma interface muito simples, então o mocking é fácil e eu farei isso. Se os objetos tiverem uma interface complexa, o escárnio será difícil e menos provável de valer a pena. Nesse caso, vamos contrastar um objeto de conexão com o banco de dados e uma estratégia de preço. A interface de estratégias de preço deve ser razoavelmente simples, esperamos que seja apenas um método CalculatePrice (SalesOrderItem). Claro, o código real pode fazer todo tipo de coisa com o SalesOrderItem, mas seu stub não precisa lidar com isso. Por outro lado, uma conexão de banco de dados tem instruções SQL sendo passadas para ele, o que lhe dá uma interface bastante complexa. Zombar do banco de dados é realmente difícil porque você verifica todas as consultas que estão sendo feitas e fornece uma resposta correta. Além disso, você não está verificando se as consultas são válidas (apenas que elas correspondem ao que você espera), nesses casos, verificar um banco de dados real faz mais sentido, dessa forma você realmente verifica se as consultas funcionam e os testes ainda passar se você reescrever as consultas para dar os mesmos resultados, mas de uma maneira diferente.

Em terceiro lugar, se um objeto for lento, eu o toco. Se você tiver um banco de dados, as chamadas para ele serão bastante lentas, solicitando o uso de um stub para evitar ter que chamá-lo. Semelhante para acesso à web, etc.

Eu apenas usei bancos de dados como um exemplo de algo que você deve copiar e também não stub. Eu stub meus bancos de dados com um banco de dados em memória sqlite que evita o problema de desempenho, mas ainda permite que meu SQL seja testado. Eu estou realmente usando um framework que gera SQL específico para o meu banco de dados para mim, então isso é trabalho.

No seu caso real, você afirma:

The majority of our unit tests are behavioural tests which often became false negative (false red) during refactoring (just because some sequence of dependencies calls changed).

Pelo que entendi, seus testes falham porque antes foo () era chamado primeiro e depois bar (). Agora bar () é chamado então foo (). Se a ordem de chamar foo () e bar () não importa, seus testes não devem verificar qual chamada primeiro. Seu teste deve apenas verificar se ambos são chamados.

    
por 09.08.2011 / 20:55
fonte
33

Refatoração - alterar a estrutura do código sem alterar o comportamento .

Teste de unidade - um teste que garante que uma unidade de código se comporte conforme o esperado.

Se seus testes de unidade falharem porque você está alterando a estrutura do código, seus testes não estarão testando o comportamento. Eles estão testando estrutura. Em teoria, se o teste falhar, ele deve ter falhado porque o comportamento testado foi alterado, independentemente da estrutura do código. Então, se seus testes estão falhando por causa da refatoração, então seus testes precisam ser escritos melhor ou sua refatoração é ruim.

Eu não acho que o problema esteja no teste unitário em geral, mas na maneira como você está escrevendo seus testes unitários. Se você achar difícil escrever seus testes de unidade para que eles testem apenas o comportamento e a estrutura não , isso é um indicador de que o design do seu código pode precisar ser alterado. E é disso que se trata o TDD.

    
por 10.08.2011 / 00:23
fonte
10

Os testes de unidade são os mais fáceis de escrever e manter quando você está escrevendo testes para funções simples. À medida que você se move para fora, para métodos mais complexos, usando estruturas de dados complexas, os testes de unidade tornam-se cada vez mais elaborados e caros. Objetos falsos devem ser empregados, e os testes ficam cada vez mais frágeis.

Em algum momento, você obtém retornos decrescentes.

No entanto, nem tudo está perdido. Praticar o TDD (que deve ser chamado de Design Dirigido por Teste, não Desenvolvimento Orientado a Testes) obriga você a pensar sobre seu código e escrevê-lo de maneira a torná-lo mais testável em unidade.

Resposta curta: A prática do teste de unidade (do qual o TDD é um subconjunto) pode melhorar muito a qualidade do código que você escreve. Mas, como a maioria das coisas na computação, não é uma bala de prata.

    
por 09.08.2011 / 20:51
fonte
6

TDD é uma mentalidade.

Criar código de maneira facilmente testável é a maneira mais fácil de ver isso. O tipo de teste deve ser irrelevante para todos, exceto para os casos mais extremos.

Unidade, Integração e Sistema são todos métodos de teste e se o seu desenvolvimento está sendo abordado de forma orientada por testes, então o tipo de teste deve ser colocado em segundo plano, pois seu código inevitavelmente percorrer vários ciclos e ser testado de maneiras variadas.

    
por 09.08.2011 / 20:49
fonte
6

Se você quiser a Verdadeira Definição de TDD, leia o Livro de Kent Beck, Desenvolvimento Orientado a Testes por Exemplo . Diferentes praticantes de TDD terão diferentes níveis de testes que eles recomendam. E pessoas diferentes têm diferentes definições de testes unitários e de integração *.

Se você está definindo testes de unidade como puramente isolados, então eu não acho que você pode dizer que o TDD é sobre testes de unidade. Algumas pessoas adotaram o termo "testes de desenvolvedor" e pararam de se preocupar com o nível em que elas existem. Eu recomendo que você se preocupe menos se seus testes forem testes de unidade e mais se forem testes úteis.

Com relação aos testes baseados em estado versus testes comportamentais / de interação / simulados, prefiro usar testes baseados em estado para itens que mantêm principalmente o estado e simular testes baseados em itens que fazem principalmente conexões. (Em outras palavras, eu uso testes baseados em estado para meus modelos e zombando de meus controladores.)

*: divulgação completa; Esse é o meu próprio link. Eu só escrevi porque ninguém mais tinha ainda, e parecia valioso apontar a terminologia de mina terrestre aqui.

    
por 09.08.2011 / 22:11
fonte
4

Em essência, o TDD é sobre feedback. O tamanho do "System Under Test" afeta apenas a precisão desse feedback. Quanto maior o SUT, maior a probabilidade de obter falsos positivos. Quanto menor o SUT, maior a probabilidade de você obter os falsos negativos que descreve. Tenho certeza de que há um equilíbrio em algum lugar.

Para resolver essa ideia de abordar o TDD com testes "maiores", recomendo a leitura de crescente software orientado a objetos, Guiado por testes

Isso pode dar uma ideia melhor de quais abordagens estão disponíveis para criar um estilo TDD de aplicativo de fora para dentro. Eu entendo que a abordagem descrita é bastante difundida e é conhecida como a "escola londrina" de TDD. Eu certamente aprendi muito sobre como configurar dispositivos de teste comuns e Fakes rolantes para as fronteiras de integração maiores que você não deseja cruzar em todos os testes. O grande exemplo do livro inicia cada recurso com um teste de aceitação que gera testes menores. A Parte IV tem muitas informações importantes sobre a construção de testes - como testar coisas como simultaneidade, sincronização e chamadas assíncronas, como tornar seus testes expressivos, o que os testes estão tentando dizer em termos de design, qualidades de um teste flexível etc. .

(Advertência: Há meia dúzia de anos, o TDD clássico significava simpatizante estatal versus "mockista". Agora, aparentemente O TDD clássico também pode usar mocks e é diferente da "London School". Independentemente da terminologia, acho que há algo a aprender fora do TDD clássico, mesmo que não se queira zombar de todas as interações com objetos.)

    
por 09.08.2011 / 23:46
fonte
3

Eu cheguei à mesma conclusão que você fez em um ponto. No entanto, depois de um tempo fazendo testes de integração (em particular testes na web usando Selenium), eu os achei doloridos também, por diferentes razões. Mais tarde eu vi este vídeo intitulado Testes de Integração são uma farsa que mais uma vez me levou ao campo de testes da unidade. Fico feliz que você tenha levantado o ponto dos falsos negativos: esse é um termo que uso também. O problema é: você também pode obter muitos falsos negativos com testes de integração. O truque, portanto, se você está escrevendo testes de unidade ou testes de integração, é minimizar o custo de manutenção dos testes e, ao mesmo tempo, obter o máximo benefício deles - permitindo adicionar recursos e refatorar o código mais rapidamente capturando erros precocemente. Isso é muito mais uma arte e eu não posso dizer que eu decifrei isso.

Os principais problemas com testes de integração são

  1. eles correm devagar, muito devagar, o que faz com que você não queira executá-los
  2. porque eles testam um grande número de objetos, uma falha pode vir de muitas causas diferentes; inversamente, um problema poderia causar um grande número de testes a falhar (falsos negativos)
  3. quando um grande número de objetos interage, o número de possíveis caminhos de código fica fora de controle. Portanto, é impraticável testar todos eles usando testes de integração.
por 10.08.2011 / 05:28
fonte
1

Pense no que os diferentes testes mostram:

Um teste unitário mostrará que a unidade de código faz o que você decidiu que deveria fazer.

Um teste de integração mostra que as unidades de código (e talvez outras coisas, como bancos de dados, sistemas de arquivos, sistemas de mensagens, etc.) trabalham juntas para fazer o que deveriam fazer.

Um teste de aceitação do usuário mostra que o usuário gosta do que todo esse código e essas coisas permitem.

Você não está realmente pronto até que as três coisas sejam boas. Não há realmente um ponto para escrever um método que não faça o que é suposto - então defina isso em um teste de unidade primeiro. Não faz sentido escrever um monte de classes que não funcionam juntas como deveriam - então defina isso em um teste de integração primeiro. Finalmente, é absolutamente necessário desenvolver um sistema que os usuários não querem, então ... você descobre isso.

    
por 09.08.2011 / 21:20
fonte
1

Recursos de testes do TDD. Às vezes, os recursos são unidades, às vezes são integrações ou testes de aceitação. Testes de TDD escala .

    
por 09.08.2011 / 22:59
fonte
1

So I would like to know how widespread is an approach of using TDD with integration tests. Is it okay? Would be grateful for any resources on this topic. I have read this article but it is a bit... strange

Desenvolvimento orientado para testes de aceitação

O termo usado para TDD envolve testar o comportamento do sistema em geral, em vez de uma unidade de código específica.

Eu pessoalmente usei isso com grande efeito por cerca de 7 anos agora, principalmente no desenvolvimento web.

Antes de você entrar, eu o aconselharia a não ir de um extremo (zombar / arrancar tudo fora da unidade) para outro (não administrar recursos externos). Você deve avaliar suas necessidades de desenvolvimento. Estabeleça quais são os trade-offs e escolha o que produzir o maior valor. Sempre faça a análise, porque as "melhores práticas" às vezes não são.

Você está bem, estou bem

Eu não me preocuparia se a prática fosse "ok". Eu acho que você deve se concentrar em se resolve o seu problema, ou pelo menos é uma melhoria acentuada.

    
por 05.03.2013 / 09:25
fonte