Inversão de dependência expande a API, resulta em testes desnecessários

8

Essa pergunta me incomoda há alguns dias e parece que várias práticas se contradizem.

Exemplo

Iteração 1

public class FooDao : IFooDao
{
    private IFooConnection fooConnection;
    private IBarConnection barConnection;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooConnection = fooConnection;
        this.barConnection = barConnection;
    }

    public Foo GetFoo(int id)
    {
        Foo foo = fooConection.get(id);
        Bar bar = barConnection.get(foo);
        foo.bar = bar;
        return foo;
    }
}

Agora, ao testar isso, eu falsificaria o IFooConnection e o IBarConnection e usaria a Injection de Dependência (DI) ao instanciar o FooDao.

Eu posso alterar a implementação, sem alterar a funcionalidade.

Iteração 2

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Agora, não vou escrever este construtor, mas imagine que ele faça a mesma coisa que o FooDao fez antes. Isso é apenas uma refatoração, portanto, obviamente, isso não altera a funcionalidade e, portanto, meu teste ainda passa.

O IFooBuilder é Interno, pois só existe para trabalhar na biblioteca, ou seja, não faz parte da API.

O único problema é que não estou mais em conformidade com a Inversão de Dependências. Se eu reescrevi isso para corrigir esse problema, pode ser assim.

Iteração 3

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Isso deve servir. Eu mudei o construtor, então meu teste precisa mudar para suportar isso (ou o contrário), mas esse não é o meu problema.

Para que isso funcione com o DI, o FooBuilder e o IFooBuilder precisam ser públicos. Isso significa que eu deveria escrever um teste para o FooBuilder, pois de repente ele se tornou parte da API da minha biblioteca. Meu problema é que os clientes da biblioteca só devem usá-lo através do meu design de API pretendido, o IFooDao, e meus testes atuam como clientes. Se eu não seguir a Inversão de dependência, meus testes e API estarão mais limpos.

Em outras palavras, tudo o que me interessa como cliente ou como teste é obter o Foo correto, não como ele é construído.

Soluções

  • Simplesmente não devo me importar, escreva os testes para o FooBuilder, mesmo que seja apenas público agradar a DI? - Suporta iteração 3

  • Devo perceber que expandir a API é uma desvantagem da Inversão de Dependência e ser muito claro sobre o motivo pelo qual optei por não cumpri-la aqui? - Suporta iteração 2

  • Coloco muita ênfase em ter uma API limpa? - Suporta iteração 3

EDIT: Quero deixar claro que meu problema não é "Como testar os internos?", Mas sim algo como "Posso mantê-lo interno e ainda estar em conformidade com o DIP, devo?".

Chris Wohlert
fonte
Posso editar minha pergunta para adicionar os testes, se quiser.
Chris Wohlert
1
"O FooBuilder e o IFooBuilder precisam ser públicos". Certamente só IFooBuilderprecisa ser público? FooBuildersó precisa ser exposto ao sistema DI através de algum tipo de método "obter implementação padrão" que retorne um IFooBuildere assim possa permanecer interno.
David Arno
Eu acho que isso poderia ser uma solução.
Chris Wohlert
2
Isso foi solicitado aqui aos programadores pelo menos uma dúzia de vezes, usando termos diferentes: "Quero fazer a diferença entre publicAPIs de biblioteca e publictestes". Ou na outra forma "Desejo manter os métodos privados para evitar que eles apareçam na API, como faço para testar esses métodos particulares?". As respostas são sempre "viva com a API pública mais ampla", "viva com testes pela API pública" ou "torne métodos internale torne-os visíveis para o código de teste".
Doc Brown
Provavelmente não substituiria o ctor, mas introduziria um novo e os amarraria.
RubberDuck

Respostas:

3

Eu iria com:

Devo perceber que expandir a API é uma desvantagem da Inversão de Dependência e ser muito claro sobre o motivo pelo qual optei por não cumpri-la aqui? - Suporta iteração 2

No entanto, em Os princípios do SOLID de OO e design ágil (a 1:08:55), o tio Bob diz que sua regra sobre injeção de dependência é não injetar tudo, você injeta apenas em locais estratégicos. (Ele também menciona que os tópicos de inversão e injeção de dependência são os mesmos).

Ou seja, a inversão de dependência não deve ser aplicada a todas as dependências de classe em um programa. Você deve fazer isso em locais estratégicos (quando paga (ou pode pagar)).

Robert Jørgensgaard Engdahl
fonte
2
Este comentário de Bob é perfeito, muito obrigado por compartilhar isso.
Chris Wohlert
5

Vou compartilhar algumas experiências / observações de várias bases de código que já vi. NB: Não estou defendendo nenhuma abordagem em particular, apenas compartilhando com você onde eu vejo esse código:

Simplesmente não devo me importar, escreva os testes para o FooBuilder, mesmo que seja apenas público agradar a DI

Antes da DI e dos testes automatizados, a sabedoria aceita era que algo deveria ser restrito ao escopo necessário. Hoje em dia, no entanto, não é incomum ver métodos que podem ser deixados internos tornados públicos para facilitar o teste (já que isso geralmente acontece em outro assembly). Nota: existe uma diretiva para expor métodos internos, mas isso equivale mais ou menos à mesma coisa.

Devo perceber que expandir a API é uma desvantagem da Inversão de Dependência e ser muito claro sobre o motivo pelo qual optei por não cumpri-la aqui?

A DI, sem dúvida, incorre em uma sobrecarga com código adicional.

Coloco muita ênfase em ter uma API limpa

Desde estruturas DI fazer introduzir código adicional, tenho notado uma mudança no sentido de código que, embora escrito no estilo DI não usa DI per se ou seja, o D de sólidos.

Em suma:

  1. Modificadores de acesso às vezes são afrouxados para simplificar o teste

  2. O DI introduz código adicional

À luz de 1 e 2, alguns desenvolvedores são da opinião de que isso é um sacrifício demais e, portanto, optam por depender de abstrações inicialmente com a opção de adaptar o DI posteriormente.

Robbie Dee
fonte
4
De fato, é por isso que alguns desenvolvedores optam pelo atributo InternalsVisibleTo para métodos internos, em vez de expô-los como públicos.
Robbie Dee #
1
"Hoje em dia, no entanto, não é incomum ver métodos que poderiam ser deixados internos tornados públicos para facilitar o teste (já que isso geralmente acontece em outra montagem)." - sério?
Brian Agnew
3
@BrianAgnew infelizmente, sim. Sua um anti-padrão juntamente com "devemos ter 100% de cobertura de teste de unidade", incluindo getters e setters :-( Por que codificadores de modo impensado hoje ?!
gbjbaanb
2
@ChrisWohlert solução simples. Não substitua o ctor. Adicione um novo. Acorrente-os juntos. Deixe seu teste existente no lugar. Ele testa implicitamente seu construtor e seu código ainda é coberto. Somente escreva testes se for valioso fazê-lo. Há um ponto em que você obtém retornos decrescentes de seu investimento.
precisa
1
@ChrisWohlert, não há absolutamente nada de errado em fornecer um construtor de conveniência. Você ainda está injetando as verdadeiras dependências da classe . Como você acha que o DI foi feito antes de todas essas estruturas de contêineres IoC "sofisticadas" aparecerem?
precisa
0

Eu particularmente não compro essa noção de que você precisa expor métodos particulares (por qualquer meio) para realizar testes adequados. Eu também sugeriria que o cliente que está enfrentando a API não é o mesmo que o método público do seu objeto, por exemplo, aqui está sua interface pública:

interface IFooDao {
   public Foo GetFoo(int id);
}

e não há menção ao seu FooBuilder

Na sua pergunta original, eu pegaria a terceira rota, usando o seu FooBuilder. Talvez eu testasse seu FooDaouso de uma simulação , concentrando-se na sua FooDaofuncionalidade (você sempre pode injetar um construtor real, se for mais fácil) e eu teria testes separados em torno do seu FooBuilder.

(Estou surpreso que a zombaria não tenha sido mencionada nesta pergunta / resposta - ela anda de mãos dadas com o teste de soluções padronizadas de DI / IoC)

Ré. seu comentário:

Conforme declarado, o construtor não fornece novas funcionalidades, portanto, não devo testá-lo, no entanto, o DIP faz parte da API, o que me obriga a testá-lo.

Não acho que isso amplie a API que você apresenta aos seus clientes. Você pode restringir a API que seus clientes usam por meio de interfaces (se desejar). Eu também sugeriria que seus testes não deveriam apenas envolver seus componentes voltados para o público. Eles devem abraçar todas as classes (implementações) que suportam essa API abaixo. por exemplo, aqui está a API REST do sistema em que estou trabalhando atualmente:

(em pseudocódigo)

void doSomeCalculation(json : CalculationRequestObject);

Eu tenho um método API e milhares de testes - a grande maioria dos quais estão relacionados aos objetos que suportam isso.

Brian Agnew
fonte
Eu particularmente não comprar essa noção de que você tem para expor métodos privados (por qualquer meio), a fim de realizar testes adequados - Eu não acredito que alguém está defendendo isso ...
Robbie Dee
Mas você disse acima - 'Modificadores de acesso às vezes são soltos para simplificar o teste'?
Brian Agnew
Os internos podem ser expostos ao conjunto de teste e a implementação implícita da interface cria métodos públicos. Eu nunca estava defendendo expondo métodos particulares ...
Robbie Dee
No meu PoV, 'Internals podem ser expostos ao conjunto de teste' equivale à mesma coisa. Eu não estou convencido de que internos de teste (se marcados como privados ou não) é um exercício que vale a pena, a não ser como um teste de regressão, enquanto refatoração mal estruturado código
Brian Agnew
Oi Brian, eu gostaria, como você, de falsificar o IFooBuilder quando analisado no FooDao, mas isso não significa que eu precisaria de outro teste para a implementação do IFooBuilder? Finalmente estamos abordando a questão real, e não se devo ou não expor os internos.
21416 Chris Wohlert #
0

Por que você deseja seguir a DI neste caso, se não for para fins de teste? Se não apenas sua API pública, mas também seus testes forem realmente mais limpos sem um IFooBuilderparâmetro (como você escreveu), não faz sentido introduzir essa classe. O DI não é um fim em si, deve ajudá-lo a tornar seu código mais testável. Se você não deseja gravar testes automatizados para esse nível específico de abstração, não aplique a DI nesse nível.

No entanto, vamos supor por um momento que você deseja aplicar o DI para permitir a criação de testes de unidade para o FooDaoconstrutor isoladamente sem o processo de construção, mas ainda não deseja um FooDao(IFooBuilder fooBuilder)construtor em sua API pública, apenas um construtor FooDao(IFooConnection fooConnection, IBarConnection barConnection). Em seguida, forneça ambos, o primeiro como "interno" e o segundo como "público":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Agora você pode manter FooBuildere IFooBuilderinterno, e aplicar o InternalsVisibleTopara torná-los acessíveis por testes de unidade.

Doc Brown
fonte
A primeira parte da sua pergunta é exatamente o que estou procurando. Uma resposta para a pergunta: "Devo usar o DIP aqui". Eu não quero esse segundo construtor. Se eu não cumprir o DIP, não preciso tornar público o IFooBuilder e posso parar de falsificá-lo / testá-lo. Em suma, você está me dizendo para furar a iteração 2.
Chris Wohlert
Honestamente, se você remover tudo do "No entanto", aceitarei a resposta. Eu acho que a primeira parte explica muito bem quando e por que eu não devo / não devo usar DI.
Chris Wohlert
Eu apenas acrescentaria a ressalva de que o DI não é apenas um teste - apenas simplifica a troca de uma concreção por outra. Se você precisa disso aqui, é algo que apenas você pode responder. Como 17 de 26 coloca maravilhosamente - em algum lugar entre YAGNI e PYIAC.
Robbie Dee
@ ChrisWohlert: Não vou remover a segunda parte só porque você não gosta de aplicá-la à sua situação. Você não precisa de testes nesse nível - tudo bem, aplique a parte 1, não use o DI aqui. Você quer testes e evita ter certas partes da API pública - tudo bem, você precisa de dois pontos de entrada no seu código com diferentes modificadores de acesso, então aplique a parte 2. Everone pode escolher a solução que melhor lhe convier.
Doc Brown
Claro que todos podem escolher a solução que desejam, mas você entendeu errado a pergunta se acha que eu queria testar o construtor. Foi disso que tentei me livrar.
Chris Wohlert