Quando usar interfaces (teste de unidade, IoC?)

17

Eu suspeito que cometi um erro de estudante aqui e estou procurando esclarecimentos. Muitas das classes na minha solução (C #) - ouso dizer a maioria - acabei escrevendo uma interface correspondente. Por exemplo, uma interface "ICalculator" e uma classe "Calculator" que a implementa, mesmo que eu nunca substitua essa calculadora por uma implementação diferente. Além disso, a maioria dessas classes reside no mesmo projeto que suas dependências - elas realmente precisam apenas estar internal, mas acabaram sendo publicum efeito colateral da implementação de suas respectivas interfaces.

Eu acho que essa prática de criar interfaces para tudo resultou de algumas falsidades:

1) Originalmente, pensei que uma interface era necessária para criar simulações de teste de unidade (estou usando o Moq), mas desde então descobri que uma classe pode ser ridicularizada se seus membros forem virtuale tem um construtor sem parâmetros (corrija-me se Estou errado).

2) Originalmente, pensei que uma interface era necessária para registrar uma classe na estrutura de IoC (Castle Windsor), por exemplo

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

quando na verdade eu poderia simplesmente registrar o tipo concreto contra ele mesmo:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) O uso de interfaces, por exemplo, parâmetros do construtor para injeção de dependência, resulta em "acoplamento frouxo".

Então, eu fiquei louco com interfaces ?! Estou ciente dos cenários em que você "normalmente" usaria uma interface, por exemplo, expondo uma API pública ou para coisas como a funcionalidade "conectável". Minha solução possui um pequeno número de classes que se encaixam nesses casos de uso, mas gostaria de saber se todas as outras interfaces são desnecessárias e devem ser removidas? Em relação ao ponto 3) acima, não estarei violando o "acoplamento solto" se eu fizer isso?

Edit : - Estou apenas brincando com o Moq, e parece exigir que os métodos sejam públicos e virtuais e tenham um construtor público sem parâmetros, para poder zombar deles. Então parece que não posso ter aulas internas?

Andrew Stephens
fonte
Tenho certeza C # não exigem que você faça um público de classe, a fim de implementar uma interface, mesmo se a interface é público ...
vaughandroid
1
Você não deveria testar membros particulares . Isso pode ser visto como um indicador do anti-padrão da classe divina. Se você sentir a necessidade de testar a funcionalidade privada da unidade, geralmente é uma boa ideia encapsular essa funcionalidade em sua própria classe.
Spoike
@O projeto em questão é um projeto de interface do usuário, onde parecia natural tornar todas as classes internas, mas elas ainda precisam de testes de unidade? Eu li em outro lugar sobre não precisar testar métodos internos, mas nunca entendi os motivos.
Andrew Stephens
Primeiro, pergunte-se qual é a unidade em teste. É toda a classe ou um método? Essa unidade precisa ser pública para ser testada adequadamente. Nos projetos de interface do usuário, normalmente separo a lógica testável em controladores / modelos de exibição / serviços que possuem métodos públicos. As visualizações / widgets reais são conectadas aos controladores / modelos de visualização / serviços e testadas através de testes regulares de fumaça (por exemplo, inicie o aplicativo e clique em). Você pode ter cenários que devem testar a funcionalidade subjacente e ter testes de unidade nos widgets leva a testes frágeis e devem ser feitos com cuidado.
Spoike
@Spoike, você está publicando seus métodos de VM apenas para torná-los acessíveis aos nossos testes de unidade? Talvez seja aí que eu esteja errado, tentando arduamente tornar tudo interno. Eu acho que minhas mãos podem estar atadas ao Moq, o que parece exigir que as classes (não apenas métodos) sejam públicas para zombar delas (veja meu comentário na resposta de Telastyn).
Andrew Stephens

Respostas:

7

Em geral, se você criar uma interface com apenas um implementador, estará escrevendo as coisas duas vezes e perdendo seu tempo. As interfaces por si só não fornecem acoplamento frouxo se estiverem fortemente acopladas a uma implementação ... Dito isto, se você quiser zombar dessas coisas em testes de unidade, isso geralmente é um ótimo sinal de que você inevitavelmente precisará de mais de um implementador em tempo real. código.

Eu consideraria um pouco de cheiro de código se quase todas as suas classes tiverem interfaces. Isso significa que eles estão quase todos trabalhando um com o outro de uma maneira ou de outra. Eu suspeitaria que as classes estão fazendo muito (já que não há classes auxiliares) ou você abstraiu demais (oh, eu quero uma interface de provedor para gravidade, pois isso pode mudar!).

Telastyn
fonte
1
Obrigado pela resposta. Como mencionado, há algumas razões equivocadas pelas quais eu fiz o que fiz, mesmo sabendo que a maioria das interfaces nunca teria mais de uma implementação. Parte do problema é que estou usando a estrutura Moq, que é ótima para zombar de interfaces, mas para zombar de uma classe, ela deve ser pública, ter um ctr público sem parâmetros e os métodos a serem zombados devem ser 'públicos virtuais'.
Andrew Stephens
A maioria das classes para as quais estou testando é interna, e não quero torná-las públicas apenas para torná-las testáveis ​​(embora você possa argumentar, foi para isso que escrevi todas essas interfaces). Se eu me livrar de minhas interfaces, provavelmente precisarei encontrar uma nova estrutura de zombaria!
Andrew Stephens
1
@ AndrewStephens - nem tudo no mundo precisa ser ridicularizado. Se as classes são sólidas (ou fortemente acopladas) o suficiente para não precisar de interfaces, elas são sólidas o suficiente para usar apenas como estão nos testes de unidade. O tempo / complexidade de moq-ing não vale os benefícios do teste.
Telastyn
-1, pois muitas vezes você não sabe quantos implementadores precisa quando escreve o código pela primeira vez. Se você usa uma interface desde o início, não precisa alterar clientes ao escrever implementadores adicionais.
Wilbert
1
@ Wilbert - claro, uma interface pequena é mais legível que a classe, mas você não está escolhendo uma ou outra, você tem a classe ou uma classe e uma interface. E você pode estar lendo demais a minha resposta. Não estou dizendo que nunca faça interfaces para uma única implementação - a maioria deve de fato, pois a maioria das interações exige essa flexibilidade. Mas leia a pergunta novamente. Fazer uma interface para tudo é apenas um cultivo de carga. IoC não é um motivo. Zombarias não são uma razão. Os requisitos são um motivo para implementar essa flexibilidade.
Telastyn
4

1) Mesmo que as classes concretas sejam ridículas, ainda é necessário que você crie membros virtuale forneça um construtor sem parâmetros, que pode ser intrusivo e indesejado. Você (ou novos membros da equipe) logo se encontrará sistematicamente adicionando virtualconstrutores sem parâmetros e em cada nova classe sem pensar duas vezes, apenas porque "é assim que as coisas funcionam".

Uma interface é uma idéia muito melhor para a IMO quando você precisa zombar de uma dependência, porque permite adiar a implementação até quando você realmente precisa dela e cria um bom contrato claro da dependência.

3) Como isso é uma falsidade?

Acredito que as interfaces são ótimas para definir protocolos de mensagens entre objetos de colaboração. Pode haver casos em que a necessidade deles seja discutível, como em entidades de domínio, por exemplo. No entanto, onde quer que você tenha um parceiro estável (por exemplo, uma dependência injetada em oposição a uma referência transitória) com a qual precisa se comunicar, considere o uso de uma interface.

guillaume31
fonte
3

A idéia da IoC é tornar os tipos de concreto substituíveis. Portanto, mesmo que (no momento) você tenha apenas uma única calculadora que implemente o ICalculator, uma segunda implementação poderá depender apenas da interface e não dos detalhes da calculadora.

Portanto, a primeira versão do seu registro IoC-Container é a correta e a que você deve usar no futuro.

Se você tornar suas aulas públicas ou internas, não está realmente relacionado, e nem o moq-ing.

Wilbert
fonte
2

Eu realmente ficaria mais preocupado com o fato de que você parece sentir a necessidade de "zombar" de todos os tipos em todo o projeto.

As duplas de teste são úteis principalmente para isolar seu código do mundo externo (bibliotecas de terceiros, E / S de disco, E / S de rede etc.) ou de cálculos realmente caros. Ser muito liberal com sua estrutura de isolamento (você realmente precisa? O seu projeto é tão complexo?) Pode facilmente levar a testes acoplados à implementação e torna seu design mais rígido. Se você escrever vários testes que verificam se alguma classe chamou o método X e Y nessa ordem e depois passou os parâmetros a, bec para o método Z, você realmente está obtendo valor? Às vezes, você faz testes como esse ao testar o log ou a interação com bibliotecas de terceiros, mas deve ser a exceção, não a regra.

Assim que sua estrutura de isolamento diz que você deve tornar as coisas públicas e que sempre deve ter construtores sem parâmetros, é hora de se esquivar, porque neste momento sua estrutura está forçando você a escrever códigos ruins (piores).

Falando mais especificamente sobre interfaces, acho que elas são úteis em alguns casos importantes:

  • Quando você precisa despachar chamadas de métodos para tipos diferentes, por exemplo, quando você tem várias implementações (essa é a óbvia, já que a definição de uma interface na maioria dos idiomas é apenas uma coleção de métodos virtuais)

  • Quando você precisar isolar o restante do seu código de alguma classe. Essa é a maneira normal de abstrair o sistema de arquivos e o acesso à rede e o banco de dados e assim por diante da lógica principal do seu aplicativo.

  • Quando você precisar de inversão de controle para proteger os limites do aplicativo. Essa é uma das maneiras mais poderosas possíveis para gerenciar dependências. Todos sabemos que os módulos de alto nível e os de baixo nível não devem saber um do outro, porque eles mudarão por diferentes motivos e você NÃO deseja ter dependências no HttpContext no modelo de domínio ou em alguma estrutura de renderização de PDF na biblioteca de multiplicação de matrizes . Fazer com que suas classes de fronteira implementem interfaces (mesmo que nunca sejam substituídas) ajuda a afrouxar o acoplamento entre as camadas e reduz drasticamente o risco de dependências penetrarem nas camadas por tipos que conhecem muitos outros tipos.

sara
fonte
1

Eu me inclino para a ideia de que, se alguma lógica for privada, a implementação dessa lógica não deve ser motivo de preocupação para ninguém e, como tal, não deve ser testada. Se alguma lógica é complexa o suficiente para você querer testá-la, essa lógica provavelmente tem um papel distinto e, portanto, deve ser pública. Por exemplo, se eu extrair algum código em um método auxiliar particular, acho que geralmente é algo que poderia estar em uma classe pública separada (um formatador, um analisador, um mapeador, o que for).
Quanto às interfaces, deixo que a necessidade de stub ou zombar da classe me leve. Coisas como repositórios, geralmente quero ser capaz de stub ou zombar. Um mapeador em um teste de integração, (geralmente) nem tanto.

Stefan Billiet
fonte