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?".
fonte
IFooBuilder
precisa ser público?FooBuilder
só precisa ser exposto ao sistema DI através de algum tipo de método "obter implementação padrão" que retorne umIFooBuilder
e assim possa permanecer interno.public
APIs de biblioteca epublic
testes". 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étodosinternal
e torne-os visíveis para o código de teste".Respostas:
Eu iria com:
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)).
fonte
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:
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.
A DI, sem dúvida, incorre em uma sobrecarga com código adicional.
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:
Modificadores de acesso às vezes são afrouxados para simplificar o teste
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.
fonte
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:
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 seuFooDao
uso de uma simulação , concentrando-se na suaFooDao
funcionalidade (você sempre pode injetar um construtor real, se for mais fácil) e eu teria testes separados em torno do seuFooBuilder
.(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:
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)
Eu tenho um método API e milhares de testes - a grande maioria dos quais estão relacionados aos objetos que suportam isso.
fonte
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
IFooBuilder
parâ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
FooDao
construtor isoladamente sem o processo de construção, mas ainda não deseja umFooDao(IFooBuilder fooBuilder)
construtor em sua API pública, apenas um construtorFooDao(IFooConnection fooConnection, IBarConnection barConnection)
. Em seguida, forneça ambos, o primeiro como "interno" e o segundo como "público":Agora você pode manter
FooBuilder
eIFooBuilder
interno, e aplicar oInternalsVisibleTo
para torná-los acessíveis por testes de unidade.fonte