Escrevendo código testável vs evitando generalidade especulativa


11

Eu estava lendo algumas postagens de blog hoje de manhã e me deparei com esta :

Se a única classe que implementa a interface Customer é CustomerImpl, você realmente não possui polimorfismo e substituibilidade, porque na prática não há nada a substituir em tempo de execução. É uma generalidade falsa.

Isso faz sentido para mim, pois implementar uma interface adiciona complexidade e, se houver apenas uma implementação, pode-se argumentar que ela adiciona complexidade desnecessária. Escrever código que é mais abstrato do que precisa ser geralmente é considerado um cheiro de código chamado "generalidade especulativa" (também mencionado no post).

Mas, se estou acompanhando o TDD, não posso (facilmente) criar duplas de teste sem essa generalidade especulativa, seja na forma de implementação da interface ou em nossa outra opção polimórfica, tornando a classe herdável e seus métodos virtuais.

Então, como reconciliamos essa troca? Vale a pena ser especulativamente geral para facilitar os testes / TDD? Se você estiver usando testes duplos, eles contam como segundas implementações e, assim, tornam a generalidade não especulativa? Você deve considerar uma estrutura de simulação mais pesada que permita a simulação de colaboradores concretos (por exemplo, Moles versus Moq no mundo C #)? Ou você deve testar com as classes concretas e escrever o que poderia ser considerado teste de "integração" até que seu projeto exija naturalmente polimorfismo?

Estou curioso para ler as opiniões de outras pessoas sobre esse assunto - desde já agradeço suas opiniões.


Pessoalmente, acho que as entidades não devem ser ridicularizadas. Eu simulei serviços apenas, e eles precisam de uma interface em qualquer caso, porque o código de domínio do código normalmente não tem referência ao código no qual os serviços são implementados.
CodesInChaos

7
Nós, usuários de idiomas tipificados dinamicamente, rimos de você atrapalhando as cadeias que seus idiomas estaticamente colocaram sobre você. Isso é uma coisa que facilita o teste de unidade em linguagens dinamicamente tipadas; não preciso ter desenvolvido uma interface para subtitular um objeto para fins de teste.
Winston Ewert

As interfaces não são usadas apenas para efetuar generalidade. Eles são usados ​​para muitos propósitos, sendo a dissociação do seu código um dos mais importantes. O que, por sua vez, facilita muito o teste do seu código.
Marjan Venema

@WinstonEwert Esse é um benefício interessante da digitação dinâmica que eu não havia considerado antes como alguém que, como você ressalta, normalmente não trabalha em linguagens de tipo dinâmico.
Erik Dietrich

@CodeInChaos Eu não tinha considerado a distinção para os fins desta pergunta, embora seja uma distinção razoável a ser feita. Nesse caso, podemos pensar em classes de serviço / estrutura com apenas uma implementação (atual). Digamos que eu tenha um banco de dados que eu acesse com DAOs. Não devo fazer interface com os DAOs até ter um modelo de persistência secundário? (Isso parece ser o que o blog autor está implicando)
Erik Dietrich

Respostas:


14

Fui ler a postagem do blog e concordo muito com o que o autor disse. No entanto, se você estiver escrevendo seu código usando interfaces para fins de teste de unidade, eu diria que a implementação simulada da interface é sua segunda implementação. Eu diria que realmente não adiciona muito em complexidade ao seu código, especialmente se a troca de não fazer isso resultar em suas classes sendo fortemente acopladas e difíceis de testar.


3
Absolutamente certo. O código de teste faz parte do aplicativo porque você precisa para obter o design, a implementação, a manutenção etc. corretos. O fato de você não enviá-lo para o cliente é irrelevante - se houver uma segunda implementação em seu conjunto de testes, a generalidade estará presente e você deverá acomodá-la.
Kilian Foth

1
Esta é a resposta que eu acho mais convincente (e o @KilianFoth acrescentando que, se o código é enviado ou não, uma segunda implementação ainda existe). Vou adiar um pouco a resposta para ver se mais alguém entra na conversa.
Erik Dietrich

Gostaria de acrescentar também, quando você está dependendo das interfaces nos testes, a generalidade não é mais especulativa
Pete

"Não fazer isso" (usando interfaces) não resulta automaticamente em suas classes serem fortemente acopladas. Simplesmente não. Por exemplo, no .NET Framework, há uma Streamclasse, mas não há um acoplamento rígido.
Luke Puplett

3

Testar código em geral não é fácil. Se assim fosse, estaríamos fazendo tudo isso há muito tempo, e não lidando com isso apenas nos últimos 10 a 15 anos. Uma das maiores dificuldades sempre foi determinar como testar o código que foi escrito de forma coesa, bem fatorada e testável sem quebrar o encapsulamento. O diretor do BDD sugere que nos concentremos quase inteiramente no comportamento e, de certa forma, parece sugerir que você realmente não precisa se preocupar com os detalhes internos em um grau tão alto, mas isso pode dificultar bastante as coisas de testar se houver muitos métodos particulares que fazem "coisas" de uma maneira muito oculta, pois podem aumentar a complexidade geral do seu teste para lidar com todos os resultados possíveis em um nível mais público.

A zombaria pode ajudar até certo ponto, mas, novamente, é bastante focada externamente. A injeção de dependência também pode funcionar muito bem, novamente com simulações ou testes duplos, mas isso também pode exigir que você exponha elementos por meio de uma interface ou diretamente, que você preferiria permanecer oculto - isso é particularmente verdadeiro se você desejar tenha um bom nível de segurança paranóica sobre certas classes dentro do seu sistema.

Para mim, o júri ainda não decidiu se deve projetar suas aulas para serem mais facilmente testáveis. Isso pode criar problemas se você precisar fornecer novos testes enquanto mantém o código legado. Aceito que você possa testar absolutamente tudo em um sistema, mas não gosto da ideia de expor - mesmo que indiretamente - os internos particulares de uma classe, apenas para que eu possa escrever um teste para eles.

Para mim, a solução sempre foi adotar uma abordagem bastante pragmática e combinar várias técnicas para atender a cada situação específica. Eu uso muitas duplas de teste herdadas para expor propriedades e comportamentos internos para meus testes. Eu zombo de tudo o que pode ser anexado às minhas classes e, quando isso não comprometer a segurança de minhas classes, fornecerei um meio de substituir ou injetar comportamentos para fins de teste. Considerarei até fornecer uma interface mais orientada a eventos se isso ajudar a melhorar a capacidade de testar o código

Onde encontro qualquer código "não testável" , procuro ver se consigo refatorar para tornar as coisas mais testáveis. Onde você tem muito código privado ocultando os bastidores, geralmente encontrará novas classes esperando para serem desdobradas. Essas classes podem ser usadas internamente, mas geralmente podem ser testadas de forma independente, com menos comportamentos privados e, geralmente, com menos camadas de acesso e complexidade. Uma coisa que eu evito, no entanto, é escrever código de produção com código de teste embutido. Pode ser tentador criar " terminais de teste " que resultem na inclusão de horrores como if testing then ..., o que indica um problema de teste não totalmente desconstruído e incompletamente resolvido.

Talvez seja útil ler o livro xUnit Test Patterns de Gerard Meszaros , que cobre todo esse tipo de coisa com muito mais detalhes do que eu posso entrar aqui. Provavelmente não faço tudo o que ele sugere, mas ajuda a esclarecer algumas das situações mais difíceis de lidar. No final do dia, você deseja satisfazer seus requisitos de teste enquanto ainda aplica seus projetos preferidos, e isso ajuda a entender melhor todas as opções para decidir melhor onde você pode se comprometer.


1

O idioma que você usa tem uma maneira de "zombar" de um objeto para teste? Nesse caso, essas interfaces irritantes podem desaparecer.

Em uma nota diferente, pode haver razões para ter um SimpleInterface e um único ComplexThing que o implementa. Pode haver partes do ComplexThing que você não deseja que sejam acessíveis ao usuário do SimpleInterface. Nem sempre é por causa de um codificador OO-ish exuberante.

Vou me afastar agora e deixar que todos peguem no fato de que o código que faz isso "cheira mal" a eles.


Sim, trabalho em linguagem (s) com estruturas de simulação que suportam a simulação de objetos concretos. Essas ferramentas requerem graus variados de salto através de aros para fazê-lo.
Erik Dietrich

0

Vou responder em duas partes:

  1. Você não precisa de interfaces se estiver interessado apenas em testar. Eu uso estruturas de simulação para esse fim (em Java: Mockito ou easymock). Acredito que o código que você cria não deve ser modificado para fins de teste. Escrever código testável equivale a escrever código modular. Por isso, costumo escrever código modular (testável) e testar apenas as interfaces públicas do código.

  2. Eu tenho trabalhado em um grande projeto Java e estou ficando profundamente convencido de que o uso de interfaces somente leitura (somente getters) é o caminho a percorrer (observe que sou um grande fã de imutabilidade). A classe de implementação pode ter setters, mas esse é um detalhe de implementação que não deve ser exposto a camadas externas. De outra perspectiva, prefiro composição do que herança (modularidade, lembra-se?), Portanto as interfaces também ajudam aqui. Estou disposto a pagar o custo da generosidade especulativa do que me dar um tiro no pé.


0

Eu tenho visto muitas vantagens desde que comecei a programar mais para uma interface além do polimorfismo.

  • Isso me obriga a pensar mais sobre a interface da classe (seus métodos públicos) e como ela irá interagir com as interfaces de outras classes.
  • Isso me ajuda a escrever turmas menores que são mais coesas e seguem o princípio de responsabilidade única.
  • É mais fácil testar meu código
  • Classes menos estáticas / estado global, pois a classe deve estar no nível da instância
  • Mais fácil de integrar e montar peças antes que todo o programa esteja pronto
  • Injeção de dependência, separando a construção do objeto da lógica de negócios

Muitas pessoas concordam que mais classes menores são melhores que menos classes maiores. Você não precisa se concentrar tanto ao mesmo tempo e cada classe tem um objetivo bem definido. Outros podem dizer que você está aumentando a complexidade tendo mais aulas.

É bom usar ferramentas para melhorar a produtividade, mas acho que confiar apenas nas estruturas Mock e, em vez de criar testabilidade e modularidade diretamente no código, resultará em código de menor qualidade a longo prazo.

Em suma, acredito que isso me ajudou a escrever um código de qualidade superior e os benefícios superam as consequências.

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.