Do ponto de vista do TDD, sou uma pessoa ruim se testar contra um endpoint ativo em vez de uma simulação?

16

Eu sigo TDD religiosamente. Meus projetos normalmente têm 85% ou mais de cobertura de teste, com casos de teste significativos.

Faço muito trabalho com o HBase , e a principal interface do cliente, HTable, é uma verdadeira chatice. Demoro 3 ou 4 vezes mais para escrever meus testes de unidade do que para escrever testes que usam um endpoint ativo.

Sei que, filosoficamente, os testes que usam zombarias devem ter prioridade sobre os testes que usam um endpoint ativo. Mas zombar do HTable é uma dor séria, e não tenho muita certeza de que ele ofereça uma grande vantagem sobre o teste em uma instância do HBase ao vivo.

Todos na minha equipe executam uma instância do HBase de nó único em suas estações de trabalho e temos instâncias do HBase de nó único em execução nas nossas caixas Jenkins, portanto, não é um problema de disponibilidade. Obviamente, os testes de endpoint ao vivo demoram mais tempo do que os testes que usam simulações, mas não nos importamos com isso.

No momento, escrevo testes de endpoint em tempo real e testes baseados em simulação para todas as minhas classes. Adoraria abandonar a zombaria, mas não quero que a qualidade diminua como resultado.

O que vocês acham?

sangfroid
fonte
8
O endpoint em tempo real não é realmente um teste de unidade, é? É um teste de integração. Mas, finalmente, é provavelmente uma questão de pragmatismo; você pode gastar o tempo escrevendo zombarias ou gastando o tempo escrevendo recursos ou corrigindo bugs.
Robert Harvey
4
Ouvi histórias de pessoas que desativavam serviços de terceiros executando um teste de unidade com seu próprio código ... que estava conectado a um endpoint ativo. A limitação de taxa não é algo que o teste de unidade normalmente faz ou se preocupa.
14
Você não é uma pessoa má. Você é uma boa pessoa fazendo uma coisa ruim.
22814 Kyralessa
15
Eu sigo TDD religiosamente Talvez esse seja o problema? Não acho que nenhuma dessas metodologias seja levada tão a sério. ;)
FrustratedWithFormsDesigner
9
Seguir o TDD religiosamente significa que você descarta o código descoberto de 15%.
Mouviciel 19/03/2014

Respostas:

23
  • Minha primeira recomendação seria não zombar de tipos que você não possui . Você mencionou que o HTable é uma verdadeira chatice de zombar - talvez você deva encapsulá-lo em um Adaptador que exponha os 20% dos recursos de HTable necessários e zombe do invólucro quando necessário.

  • Dito isto, vamos supor que estamos falando de tipos que todos vocês possuem. Se seus testes baseados em simulação são focados em cenários de caminhos felizes, onde tudo corra bem, você não os perderá, porque seus testes de integração provavelmente já estão testando exatamente os mesmos caminhos.

    No entanto, testes isolados se tornam interessantes quando você começa a pensar em como o sistema em teste deve reagir a tudo o que pode acontecer conforme definido no contrato do colaborador, independentemente do objeto concreto real com o qual está falando. Isso faz parte do que alguns chamam de correção básica . Pode haver muitos desses pequenos casos e muitas mais combinações deles. É aqui que os testes de integração começam a ficar ruins, enquanto os testes isolados permanecem rápidos e gerenciáveis.

    Para ser mais concreto, o que acontece se um dos métodos do seu adaptador HTable retornar uma lista vazia? E se retornar nulo? E se lançar uma exceção de conexão? Deve ser definido no contrato do Adaptador se alguma dessas coisas pode acontecer e qualquer um de seus consumidores deve estar preparado para lidar com essas situações , daí a necessidade de testes para elas.

Resumindo: você não verá nenhum declínio na qualidade removendo seus testes baseados em simulação se eles testarem exatamente as mesmas coisas que seus testes de integração . No entanto, tentar imaginar testes isolados adicionais (e testes de contrato ) pode ajudá-lo a pensar em suas interfaces / contratos extensivamente e aumentar a qualidade, resolvendo defeitos que seriam difíceis de pensar e / ou lentos para testar com testes de integração.

guillaume31
fonte
+1 Acho muito mais fácil construir casos extremos com testes simulados do que preencher o banco de dados com esses casos.
19414 Rob
Concordo com a maior parte da sua resposta. No entanto, não tenho certeza se concordo com a parte do adaptador. HTable é uma chatice porque é bastante simples. Por exemplo, se você deseja executar uma operação de obtenção em lote, deve criar vários objetos Get, colocá-los em uma lista e chamar HTable.batch (). De uma perspectiva de zombaria, isso é um problema sério, porque você precisa criar um Matcher personalizado que examine a lista de objetos get que você passa para HTable.batch () e, em seguida, retorne os resultados corretos para essa lista de objetos get (). Uma dor séria.
precisa
Suponho que eu poderia criar uma classe de empacotador agradável e amigável para o HTable que cuidasse de toda essa limpeza, mas naquele momento ... eu sinto que estou meio que criando uma estrutura em torno do HTable, e esse deveria ser realmente o meu trabalho? Geralmente "Vamos construir uma estrutura!" é um sinal de que estou indo na direção errada. Eu poderia passar dias escrevendo aulas para tornar o HBase mais amigável e não sei se esse é um ótimo uso do meu tempo. Além disso, estou pesquisando uma interface ou invólucro em vez de apenas o objeto HTable antigo e simples, e isso definitivamente tornaria meu código mais complexo.
precisa
Mas eu concordo com o seu ponto principal, que não se deve escrever zombarias para as classes que eles não possuem. E definitivamente concordo que não faz sentido escrever um teste simulado que testa a mesma coisa que um teste de integração. Parece que as simulações são melhores para testar interfaces / contratos. Obrigado pelo conselho - isso me ajudou muito!
precisa
Eu tenho pouca idéia do que o HTable realmente faz e como você o usa, portanto, não leve meu exemplo de wrapper à letra. Eu havia mencionado um invólucro / adaptador porque achava que o invólucro era relativamente pequeno. Você não precisa introduzir uma réplica um para um no HTable, o que certamente seria uma dor, muito menos uma estrutura inteira - mas você precisa de uma costura , uma interface entre o domínio do seu aplicativo e o do HTable. Ele deve reformular algumas das funcionalidades do HTable nos próprios termos do seu aplicativo. O padrão do Repositório é uma encarnação perfeita de tal costura quando se trata de acesso a dados.
precisa
11

filosoficamente, os testes que usam zombarias devem ter prioridade sobre os testes que usam um terminal ativo

Eu acho que, no mínimo, esse é um ponto de controvérsia atual entre os defensores do TDD.

Minha visão pessoal vai além disso, para dizer que um teste baseado em simulação é principalmente uma maneira de representar uma forma de contrato de interface ; idealmente, ele quebra (ou seja, falha) se e somente se você alterar a interface . E, como tal, em linguagens razoavelmente fortemente tipadas como Java, e ao usar uma interface definida explicitamente, é quase inteiramente supérfluo: o compilador já terá lhe informado se você mudou a interface.

A principal exceção é quando você está usando uma interface muito genérica, talvez baseada em anotações ou reflexões, que o compilador não é capaz de policiar de maneira útil automaticamente. Mesmo assim, você deve verificar se há uma maneira de fazer a validação programaticamente (eq uma biblioteca de verificação de sintaxe SQL), e não manualmente, usando zombarias.

É nesse último caso que você está realizando ao testar com um banco de dados local 'ativo'; a implementação htable entra em ação e aplica uma validação muito mais abrangente do contrato interfacve do que você jamais imaginaria escrever manualmente.

Infelizmente, um uso muito mais comum de testes baseados em simulação é o teste que:

  • passa por qualquer que seja o código no momento em que o teste foi escrito
  • não fornece garantias sobre nenhuma outra propriedade do código além da sua existência e tipo de execução
  • falha sempre que você altera esse código

É claro que esses testes devem ser excluídos à vista.

soru
fonte
1
Eu não posso apoiar isso o suficiente. Prefiro ter 1pc de cobertura com ótimos testes do que 100pc de preenchimento.
Ian
3
Testes baseados em simulação de fato descrevem o contrato usado por dois objetos para conversar juntos, mas vão muito além do que o sistema de tipos de uma linguagem como Java pode fazer. Não se trata apenas de assinaturas de métodos, eles também podem especificar intervalos de valores válidos para argumentos ou resultados retornados, quais exceções são aceitáveis, em que ordem e quantas vezes os métodos podem ser chamados etc. O compilador sozinho não avisará se houver são mudanças nessas. Nesse sentido, não acho que sejam supérfluos. Consulte infoq.com/presentations/integration-tests-scam para obter mais informações sobre testes baseados em simulação.
precisa
1
... concordam ou seja, testar a lógica em torno da chamada de interface
Rob
1
Certamente pode adicionar exceções não verificadas, pré-condições não declaradas e estado implícito à lista de itens que tornam uma interface menos tipicamente estaticamente e justificam testes baseados em simulação em vez de compilação simples. O problema, porém, é que quando os aspectos fazer a mudança, sua especificação está implícita e distribuído entre os testes de todos os clientes. É provável que não sejam atualizados e, portanto, fique sentado em silêncio, escondendo um bug atrás de um sinal verde.
soru
"a especificação deles está implícita": não se você escrever testes de contrato para suas interfaces ( blog.thecodewhisperer.com/2011/07/07/contract-tests-an-example ) e cumpri-los ao configurar zombarias.
precisa
5

Quanto tempo leva para executar um teste baseado em terminal do que um teste baseado em simulação? Se for significativamente mais longo, sim, vale a pena o investimento do seu tempo para escrever testes para tornar os testes de unidade mais rápidos - porque você precisará executá-los muitas e muitas vezes. Se não for significativamente mais longo, mesmo que os testes baseados em pontos de extremidade não sejam testes de unidade "puros", desde que eles estejam fazendo um bom trabalho de teste da unidade, não há razão para ser religioso sobre isso.

Carl Manaster
fonte
4

Concordo totalmente com a resposta de guillaume31, nunca zombe de tipos que você não possui!

Normalmente, uma dor no teste (zombando de uma interface complexa) reflete um problema no seu design. Talvez você precise de alguma abstração entre seu modelo e seu código de acesso a dados; exemplo de formulário, usando uma arquitetura hexagonal e um padrão de repositório, é a maneira mais comum de resolver esse tipo de problema.

Se você deseja fazer um teste de integração para verificar as coisas, faça um teste de integração, se você quiser fazer um teste de unidade, porque você está testando sua lógica, faça um teste de unidade e isole a persistência. Mas, ao fazer um teste de integração, porque você não sabe como isolar sua lógica de um sistema externo (ou porque isolar uma dor) é um grande cheiro, você está escolhendo a integração em vez da unidade para limitar o design, não para uma necessidade real para testar a integração.

Dê uma olhada nesta palestra de Ian Cooper: http://vimeo.com/68375232 , ele fala sobre arquitetura hexagonal e testes, ele fala sobre quando e o que zombar, um discurso realmente inspirado que resolve muitas perguntas como a sua sobre o TDD real .

AlfredoCasado
fonte
1

TL; DR - Na minha opinião, depende de quanto esforço você gasta nos testes e se seria melhor gastar mais no sistema atual.

Versão longa:

Algumas boas respostas aqui, mas minha opinião é diferente: o teste é uma atividade econômica que precisa se compensar e se o tempo que você gasta não é devolvido no desenvolvimento e na confiabilidade do sistema (ou qualquer outra coisa que você deseja obter) de testes), então você pode estar fazendo um mau investimento; você está no negócio de construir sistemas, não escrevendo testes. Portanto, reduzir o esforço para escrever e manter testes é crucial.

Por exemplo, alguns valores principais que ganho com testes são:

  • Confiabilidade (e, portanto, velocidade de desenvolvimento): refatorar código / integrar uma nova estrutura / trocar um componente / porta para uma plataforma diferente, ter certeza de que as coisas ainda funcionam
  • Feedback do projeto: feedback clássico do TDD / BDD "use your code" em suas interfaces de nível baixo / médio

Os testes em relação a um endpoint ativo ainda devem fornecê-los.

Algumas desvantagens do teste em um terminal ativo:

  • Configuração do ambiente - configurar e padronizar o ambiente de execução de teste é mais trabalhoso, e configurações de ambiente sutilmente diferentes podem resultar em comportamento sutilmente diferente
  • Apatridia - trabalhar com um endpoint ativo pode acabar promovendo testes de gravação que dependem de um estado mutável do endpoint, que é frágil e difícil de raciocinar (ou seja, quando algo está falhando, está falhando por causa de um estado estranho?)
  • O ambiente de execução de teste é frágil - se um teste falhar, é o teste, o código ou o ponto final ativo?
  • Velocidade de execução - um ponto final ativo geralmente é mais lento e às vezes é mais difícil de paralelizar
  • Criando casos extremos para testes - geralmente triviais com simulação, às vezes com problemas com um terminal ativo (por exemplo, os complicados de configurar são erros de transporte / HTTP)

Se eu estivesse nessa situação, e as desvantagens não parecessem um problema, ao zombar consideravelmente do ponto de extremidade que retardava consideravelmente a escrita dos meus testes, eu testaria contra um ponto de extremidade ativo em um piscar de olhos, desde que eu tivesse certeza de que verifique novamente depois de um tempo para ver se os inconvenientes não se tornam um problema na prática.

orip
fonte
1

De uma perspectiva de teste, existem alguns requisitos que são uma necessidade absoluta:

  • Os testes (unitários ou não) nunca devem ter uma maneira de tocar nos dados de produção
  • Os resultados de um teste nunca devem afetar os resultados de outro teste
  • Você deve sempre começar de uma posição conhecida

Esse é um grande desafio ao se conectar a qualquer fonte que mantenha o estado fora dos seus testes. Não é um TDD "puro", mas a equipe do Ruby on Rails resolveu esse problema de uma maneira que pode ser adaptada para seus propósitos. A estrutura de teste do rails funcionou desta maneira:

  • A configuração de teste foi selecionada automaticamente ao executar testes de unidade
  • O banco de dados foi criado e inicializado no início da execução dos testes de unidade
  • O banco de dados foi excluído após a execução dos testes de unidade
  • Se você estiver usando o SqlLite, a configuração de teste utilizará um banco de dados de RAM

Todo esse trabalho foi incorporado ao equipamento de teste e funciona razoavelmente bem. Há muito mais, mas o básico é suficiente para esta conversa.

Nas diferentes equipes com as quais trabalhei ao longo do tempo, faríamos escolhas que promovessem o teste do código, mesmo que esse não fosse o caminho mais correto. Idealmente, agruparíamos todas as chamadas para um armazenamento de dados com o código que controlamos. Em teoria, se algum desses projetos antigos obtivesse novo financiamento, poderíamos voltar e transferi-los de vinculados ao banco de dados para o Hadoop, concentrando nossa atenção em apenas algumas classes.

Os aspectos importantes não são para mexer com os dados de produção e verifique se você está realmente testando o que pensa estar testando. É realmente importante poder redefinir o serviço externo para uma linha de base conhecida sob demanda - mesmo a partir do seu código.

Berin Loritsch
fonte