Comportamentos de teste de unidade sem acoplamento aos detalhes da implementação


14

Em sua palestra TDD, onde tudo deu errado , Ian Cooper enfatiza a intenção original de Kent Beck por trás dos testes de unidade no TDD (para testar comportamentos, não métodos de classes especificamente) e argumenta para evitar o acoplamento dos testes à implementação.

No caso de comportamento como save X to some data sourceem um sistema com um conjunto típico de serviços e repositórios, como podemos testar a unidade para salvar alguns dados no nível de serviço, através do repositório, sem acoplar o teste aos detalhes da implementação (como chamar um método específico )? Evitar esse tipo de acoplamento realmente não vale o esforço / mal, de alguma forma?


1
Se você deseja testar se os dados foram salvos no repositório, o teste terá que realmente ir e verificar o repositório para ver se os dados estão lá, certo? Ou eu estou esquecendo de alguma coisa?

Minha pergunta era mais sobre evitar acoplar os testes a um detalhe de implementação, como chamar um método específico no repositório, ou realmente se isso é algo que deve ser feito.
Andy Hunt

Respostas:


8

Seu exemplo específico é um caso em que você geralmente precisa testar verificando se um determinado método foi chamado, porque saving X to data sourcesignifica se comunicar com uma dependência externa ; portanto, o comportamento que você deve testar é que a comunicação está ocorrendo conforme o esperado .

No entanto, isso não é uma coisa ruim. As interfaces de fronteira entre seu aplicativo e suas dependências externas não são detalhes de implementação ; na verdade, elas são definidas na arquitetura do seu sistema; o que significa que é provável que esse limite não seja alterado (ou, se necessário, seria o tipo de alteração menos frequente). Portanto, acoplar seus testes a uma repositoryinterface não deve causar muitos problemas (se houver, considere se a interface não está roubando responsabilidades do aplicativo).

Agora, considere apenas as regras de negócios de um aplicativo, dissociadas da interface do usuário, bancos de dados e outros serviços externos. É aqui que você deve estar livre para alterar a estrutura e o comportamento do código. É aqui que os testes de acoplamento e os detalhes da implementação o forçam a alterar mais código de teste que código de produção, mesmo quando não há alteração no comportamento geral do aplicativo. É aqui que testar em Statevez de Interactionnos ajudar a ir mais rápido.

PS: Não é minha intenção dizer se o teste por Estado ou Interações é a única maneira verdadeira de TDD - acredito que seja uma questão de usar a ferramenta certa para o trabalho certo.


Quando você menciona "comunicação com uma dependência externa", refere-se a dependências externas como aquelas externas à unidade em teste ou externas ao sistema como um todo?
Andy Hunt

Por "dependência externa", quero dizer qualquer coisa que você possa considerar como um plug-in para seu aplicativo. Por aplicativo, quero dizer as regras de negócios, independentes de qualquer tipo de detalhe, como qual estrutura usar para persistência ou interface do usuário. Eu acho que o tio Bob pode explicar melhor, como neste Discussão: youtube.com/watch?v=WpkDN78P884
MichelHenrich

Eu acho que essa é a abordagem ideal, como diz a palestra, para testar com base em "características" ou "comportamento" e um teste por característica ou comportamento (ou permutação de uma, ou seja, parâmetros variáveis). No entanto, se eu tiver um teste "feliz" para um recurso, para fazer o TDD, isso significa que terei um único commit gigante (e revisão de código) para esse recurso, o que é uma má idéia. Como isso seria evitado? Escreva uma parte desse recurso como um teste e todo o código associado a ele e adicione gradualmente o restante do recurso nas confirmações subsequentes?
Jordan

Eu realmente gostaria de ver um exemplo do mundo real de testes que estão acoplados à implementação.
PositiveGuy

7

Minha interpretação dessa palestra é:

  • componentes de teste, não classes.
  • componentes de teste através de suas portas de interface.

Não está indicado na conversa, mas acho que o contexto assumido para o conselho é algo como:

  • você está desenvolvendo um sistema para usuários, não, digamos, uma biblioteca ou estrutura utilitária.
  • o objetivo do teste é entregar com êxito o máximo possível dentro de um orçamento competitivo.
  • Os componentes são gravados em uma única linguagem madura, provavelmente tipicamente estaticamente, como C # / Java.
  • um componente é da ordem de 10000-50000 linhas; um projeto Maven ou VS, plug-in OSGI etc.
  • componentes são escritos por um único desenvolvedor ou equipe intimamente integrada.
  • você está seguindo a terminologia e a abordagem de algo como a arquitetura hexagonal
  • uma porta de componente é onde você deixa o idioma local e seu sistema de tipos para trás, alternando para http / SQL / XML / bytes / ...
  • agrupar todas as portas são interfaces digitadas, no sentido Java / C #, que podem ter implementações implementadas para alternar tecnologias.

Portanto, testar um componente é o maior escopo possível no qual algo ainda pode ser chamado razoavelmente de teste de unidade. Isso é bem diferente de como algumas pessoas, especialmente acadêmicos, usam o termo. Não é nada parecido com os exemplos no tutorial típico da ferramenta de teste de unidade. No entanto, corresponde à sua origem nos testes de hardware; placas e módulos são testados em unidade, não fios e parafusos. Ou pelo menos você não constrói uma Boeing simulada para testar um parafuso ...

Extrapolando disso, e jogando em alguns dos meus próprios pensamentos,

  • Toda interface será uma entrada, uma saída ou um colaborador (como um banco de dados).
  • você testa as interfaces de entrada; chame os métodos, afirme os valores de retorno.
  • você zomba das interfaces de saída; verifique se os métodos esperados são chamados para um determinado caso de teste.
  • você finge os colaboradores; fornecer uma implementação simples, mas funcional

Se você fizer isso de maneira adequada e limpa, mal precisará de uma ferramenta de zombaria; ele é usado apenas algumas vezes por sistema.

Um banco de dados geralmente é um colaborador, portanto, é falsificado em vez de ridicularizado. Isso seria doloroso de implementar manualmente; felizmente, essas coisas já existem .

O padrão de teste básico é executar algumas seqüências de operações (por exemplo, salvar e recarregar um documento); confirme que funciona. É o mesmo que para qualquer outro cenário de teste; nenhuma alteração (de trabalho) na implementação provavelmente fará com que esse teste falhe.

A exceção é onde os registros do banco de dados são gravados, mas nunca lidos pelo sistema em teste; por exemplo, registros de auditoria ou similares. Essas são saídas e, portanto, devem ser ridicularizadas. O padrão de teste é fazer alguma sequência de operações; confirme se a interface de auditoria foi chamada com métodos e argumentos, conforme especificado.

Observe que, mesmo aqui, desde que você esteja usando uma ferramenta de simulação de segurança de tipo como mockito , renomear um método de interface não pode causar uma falha no teste. Se você usar um IDE com os testes carregados, ele será refatorado junto com o método renomear. Caso contrário, o teste não será compilado.


Você pode descrever / me dar um exemplo concreto de uma porta de interface?
PositiveGuy

o que é um exemplo de uma interface de saída. Você pode ser específico no código? Mesmo com interface de entrada.
PositiveGuy

Uma interface (no sentido Java / C #) envolve uma porta, que pode ser qualquer coisa que fale com o mundo externo (d / b, soquete, http, ....). Uma interface de saída é aquela que não possui métodos com valores de retorno provenientes do mundo externo via porta, apenas exceções ou equivalentes.
soru 5/09/15

Uma interface de entrada é o oposto, um colaborador é entrada e saída.
soru 5/09/15

1
Eu acho que você está falando sobre uma abordagem de design e um conjunto de terminologias completamente diferentes dos descritos no vídeo. Mas 90% das vezes um repositório (ou seja, um banco de dados) é um colaborador, não uma entrada ou saída. E, portanto, a interface é uma interface de colaboração.
soru 13/09/2015

0

Minha sugestão é usar uma abordagem de teste baseada em estado:

Dado Temos o banco de dados de teste em um estado conhecido

QUANDO O serviço é chamado com argumentos X

ENTÃO Afirme que o banco de dados mudou de seu estado original para o estado esperado chamando métodos de repositório somente leitura e verificando seus valores retornados

Dessa maneira, você não depende de nenhum algoritmo interno do serviço e pode refatorar sua implementação sem precisar alterar os testes.

O único acoplamento aqui é a chamada do método de serviço e as chamadas do repositório necessárias para ler os dados do banco de dados, o que é bom.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.