Esta pregunta me ha molestado durante unos días y parece que varias prácticas se contradicen entre sí.
Ejemplo
Iteración 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;
}
}
Ahora, al probar esto, falsificaría IFooConnection e IBarConnection, y usaría Inyección de dependencia (DI) al crear instancias de FooDao.
Puedo cambiar la implementación, sin cambiar la funcionalidad.
Iteración 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);
}
}
Ahora, no escribiré este constructor, pero imagina que hace lo mismo que FooDao hizo antes. Esto es solo una refactorización, por lo que, obviamente, esto no cambia la funcionalidad, por lo que mi prueba aún pasa.
IFooBuilder es interno, ya que solo existe para trabajar para la biblioteca, es decir, no es parte de la API.
El único problema es que ya no cumplo con la Inversión de dependencia. Si reescribí esto para solucionar ese problema, podría verse así.
Iteración 3
public class FooDao : IFooDao
{
private IFooBuilder fooBuilder;
public FooDao(IFooBuilder fooBuilder)
{
this.fooBuilder = fooBuilder;
}
public Foo GetFoo(int id)
{
return fooBuilder.Build(id);
}
}
Esto debería hacerlo. He cambiado el constructor, por lo que mi prueba debe cambiar para admitir esto (o al revés), sin embargo, este no es mi problema.
Para que esto funcione con DI, FooBuilder e IFooBuilder deben ser públicos. Esto significa que debería escribir una prueba para FooBuilder, ya que de repente se convirtió en parte de la API de mi biblioteca. Mi problema es que los clientes de la biblioteca solo deberían usarla a través de mi diseño de API previsto, IFooDao, y mis pruebas actúan como clientes. Si no sigo la Inversión de dependencia, mis pruebas y API están más limpios.
En otras palabras, todo lo que me importa como cliente, o como prueba, es obtener el Foo correcto, no cómo se construye.
Soluciones
¿Debería simplemente no importarme, escribir las pruebas para FooBuilder aunque solo sea público para complacer a DI? - Soporta iteración 3
¿Debo darme cuenta de que expandir la API es una desventaja de la Inversión de dependencia, y tener muy claro por qué elegí no cumplir con esto aquí? - Soporta la iteración 2
¿Pongo demasiado énfasis en tener una API limpia? - Soporta iteración 3
EDITAR: Quiero dejar en claro que mi problema no es "¿Cómo probar las partes internas?", Sino algo como "¿Puedo mantenerlo interno y seguir cumpliendo con DIP, y debería?".
fuente
IFooBuilder
necesita ser público?FooBuilder
solo necesita exponerse al sistema DI a través de algún tipo de método de "obtención de implementación predeterminada" que devuelve unIFooBuilder
y, por lo tanto, puede permanecer interno.public
API de la biblioteca y laspublic
pruebas". O en la otra forma "Quiero mantener los métodos privados para evitar que aparezcan en la API, ¿cómo pruebo esos métodos privados?". Las respuestas son siempre "vivir con la API pública más amplia", "vivir con pruebas a través de la API pública" o "crear métodosinternal
y hacerlos visibles para el código de prueba".Respuestas:
Yo iría con:
Sin embargo, en The SOLID Principles of OO and Agile Design (a at 1:08:55), el tío Bob dice que su regla sobre la inyección de dependencia es no inyectar todo, inyecta solo en ubicaciones estratégicas. (También menciona que los temas de inversión de dependencia e inyección de dependencia son los mismos).
Es decir, la inversión de dependencia no está destinada a aplicarse en todas las dependencias de clase en un programa. Debe hacerlo en ubicaciones estratégicas (cuando paga (o puede pagar)).
fuente
Voy a compartir algunas experiencias / observaciones de varias bases de código que he visto. NB: no estoy abogando por ningún enfoque en particular, solo comparto contigo dónde veo que va ese código:
Antes de la DI y las pruebas automatizadas, la sabiduría aceptada era que algo debería restringirse al alcance requerido. Sin embargo, hoy en día no es raro ver que los métodos que podrían dejarse internos se hagan públicos para facilitar las pruebas (ya que esto generalmente ocurre en otro ensamblaje). Nota: existe una directiva para exponer métodos internos, pero esto equivale más o menos a lo mismo.
DI indudablemente incurre en una sobrecarga con código adicional.
Dado que los marcos DI hacen introducir un código adicional, he notado un cambio hacia código que, si bien está escrito en el estilo de DI no utiliza DI per se es decir, el D del estado sólido.
En resumen:
Los modificadores de acceso a veces se aflojan para simplificar las pruebas
DI introduce código adicional
A la luz de 1 y 2, algunos desarrolladores opinan que esto es demasiado sacrificio y, por lo tanto, simplemente eligen depender de abstracciones inicialmente con la opción de volver a ajustar DI más adelante.
fuente
En particular, no creo que tenga que exponer métodos privados (por cualquier medio) para realizar las pruebas adecuadas. También sugeriría que su cliente que enfrenta la API no es lo mismo que el método público de su objeto, por ejemplo, aquí está su interfaz pública:
y no hay mención de tu
FooBuilder
En su pregunta original, tomaría la tercera ruta, usando su
FooBuilder
. Quizás probaría suFooDao
uso de un simulacro , concentrándome en suFooDao
funcionalidad (siempre puede inyectar un generador real si es más fácil) y tendría pruebas separadas alrededor de ustedFooBuilder
.(Me sorprende que la burla no se haya mencionado en esta pregunta / respuesta, va de la mano con la prueba de soluciones con patrones DI / IoC)
Re. tu comentario:
No creo que esto amplíe la API que presentas a tus clientes. Puede restringir la API que usan sus clientes a través de interfaces (si así lo desea). También sugeriría que sus pruebas no solo rodeen sus componentes públicos. Deben abarcar todas las clases (implementaciones) que admiten esa API debajo. Por ejemplo, aquí está la API REST para el sistema en el que estoy trabajando actualmente:
(en pseudocódigo)
Tengo un método API y miles de pruebas, la gran mayoría de las cuales están alrededor de los objetos que lo respaldan.
fuente
¿Por qué desea seguir la DI en este caso si no es para fines de prueba? Si no solo su API pública, sino también sus pruebas son realmente más limpias sin un
IFooBuilder
parámetro (como escribió), no tiene sentido introducir dicha clase. DI no es un fin en sí mismo, debería ayudarlo a hacer que su código sea más comprobable. Si no desea escribir pruebas automatizadas para ese nivel particular de abstracción, no aplique DI a ese nivel.Sin embargo, supongamos por un momento que desea aplicar DI para permitir crear pruebas unitarias para el
FooDao
constructor de forma aislada sin el proceso de construcción, pero todavía no quiere unFooDao(IFooBuilder fooBuilder)
constructor en su API pública, solo un constructorFooDao(IFooConnection fooConnection, IBarConnection barConnection)
. Luego proporcione ambos, el primero como "interno", el segundo como "público":Ahora se puede mantener
FooBuilder
yIFooBuilder
interna, y aplicar elInternalsVisibleTo
para que sean accesibles por las pruebas unitarias.fuente