La inversión de dependencia expande la API, lo que resulta en pruebas innecesarias

8

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?".

Chris Wohlert
fuente
Puedo editar mi pregunta para agregar las pruebas si lo desea.
Chris Wohlert
1
"FooBuilder e IFooBuilder deben ser públicos". ¿Seguramente solo IFooBuildernecesita ser público? FooBuildersolo necesita exponerse al sistema DI a través de algún tipo de método de "obtención de implementación predeterminada" que devuelve un IFooBuildery, por lo tanto, puede permanecer interno.
David Arno
Supongo que eso podría ser una solución.
Chris Wohlert
2
Esto se ha preguntado aquí en Programadores al menos una docena de veces, usando diferentes términos: "Quiero hacer una diferencia entre las publicAPI de la biblioteca y las publicpruebas". 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étodos internaly hacerlos visibles para el código de prueba".
Doc Brown
Probablemente no reemplazaría el ctor, pero introduciría uno nuevo y los encadenaría.
RubberDuck

Respuestas:

3

Yo iría con:

¿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

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)).

Robert Jørgensgaard Engdahl
fuente
2
Este comentario de Bob es perfecto, muchas gracias por compartir esto.
Chris Wohlert
5

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:

Si simplemente no me importa, escriba las pruebas para FooBuilder aunque solo sea público para complacer a DI

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.

¿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í?

DI indudablemente incurre en una sobrecarga con código adicional.

¿Pongo demasiado énfasis en tener una API limpia?

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:

  1. Los modificadores de acceso a veces se aflojan para simplificar las pruebas

  2. 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.

Robbie Dee
fuente
44
De hecho, es por eso que algunos desarrolladores optan por el atributo InternalsVisibleTo para métodos internos en lugar de exponerlos como públicos.
Robbie Dee
1
"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 la prueba (ya que esto generalmente ocurre en otro ensamblaje)".
Brian Agnew
3
@BrianAgnew por desgracia sí. Es un anti-patrón junto con "debemos tener el 100% de cobertura de prueba de unidad", que incluye captadores y definidores :-( ¿Por qué los codificadores de manera irreflexiva hoy ?!
gbjbaanb
2
@ChrisWohlert solución simple. No reemplace el ctor. Agrega uno nuevo. Encadenarlos juntos. Deje su prueba existente en su lugar. Implícitamente prueba su constructor y su código aún está cubierto. Solo escriba pruebas si es valioso hacerlo. Hay un punto en el que obtiene rendimientos decrecientes de su inversión.
RubberDuck
1
@ChrisWohlert no hay absolutamente nada de malo en proporcionar un constructor conveniente. Todavía estás inyectando las verdaderas dependencias de la clase . ¿Cómo crees que se realizó la DI antes de que aparecieran todos esos marcos de contenedores IoC "elegantes"?
RubberDuck
0

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:

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

y no hay mención de tu FooBuilder

En su pregunta original, tomaría la tercera ruta, usando su FooBuilder. Quizás probaría su FooDaouso de un simulacro , concentrándome en su FooDaofuncionalidad (siempre puede inyectar un generador real si es más fácil) y tendría pruebas separadas alrededor de usted FooBuilder.

(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:

Como se indicó, el constructor no proporciona una nueva funcionalidad, por lo que no debería necesitar probarlo, sin embargo, DIP lo hace parte de la API, lo que me obliga a probarlo.

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)

void doSomeCalculation(json : CalculationRequestObject);

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.

Brian Agnew
fuente
Particularmente no me comprar esta idea de que hay que exponer los métodos privados (por cualquier medio) con el fin de realizar las pruebas adecuadas - Yo no creo que nadie está abogando por esto ...
Robbie Dee
Pero usted dijo lo anterior: "¿Los modificadores de acceso a veces se sueltan para simplificar las pruebas"?
Brian Agnew
Las partes internas pueden exponerse al ensamblaje de prueba y la implementación de interfaz implícita crea métodos públicos. Nunca estuve abogando por exponer métodos privados ...
Robbie Dee
Desde mi punto de vista, 'Las partes internas pueden estar expuestas al conjunto de prueba' equivale a lo mismo. No estoy convencido de que probar las partes internas (ya sea marcadas como privadas o no) sea un ejercicio que valga la pena, a menos que sea como una prueba de regresión mientras se refactoriza el código mal estructurado
Brian Agnew
Hola Brian, quisiera, como tú, fingir el IFooBuilder cuando se analiza a FooDao, pero ¿no significa esto que necesitaría otra prueba para la implementación de IFooBuilder? Finalmente nos estamos acercando al problema real, y no si debo exponer o no las partes internas.
Chris Wohlert
0

¿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 IFooBuilderpará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 FooDaoconstructor de forma aislada sin el proceso de construcción, pero todavía no quiere un FooDao(IFooBuilder fooBuilder)constructor en su API pública, solo un constructor FooDao(IFooConnection fooConnection, IBarConnection barConnection). Luego proporcione ambos, el primero como "interno", el 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);
    }
}

Ahora se puede mantener FooBuildery IFooBuilderinterna, y aplicar el InternalsVisibleTopara que sean accesibles por las pruebas unitarias.

Doc Brown
fuente
La primera parte de su pregunta es exactamente lo que estoy buscando. Una respuesta a la pregunta: "¿Debo usar DIP aquí". Sin embargo, no quiero ese segundo constructor. Si no cumplo con DIP, no necesito hacer público IFooBuilder, y puedo dejar de fingirlo / probarlo. En resumen, me estás diciendo que me quede con la iteración 2.
Chris Wohlert
Honestamente, si eliminas todo de "Sin embargo", aceptaré la respuesta. Creo que la primera parte explica muy bien cuándo y por qué debería / no debería usar DI.
Chris Wohlert
Solo agregaría la advertencia de que DI no se trata solo de probar, simplemente simplifica el intercambio de una concreción con otra. En cuanto a si necesita esto, esto es algo que solo usted puede responder. Como 17 de 26 lo pone maravillosamente, en algún lugar entre YAGNI y PYIAC.
Robbie Dee
@ChrisWohlert: No voy a eliminar la segunda parte solo porque no te gusta aplicarla a tu situación. No necesita pruebas en ese nivel: bien, aplique la parte 1, no use DI aquí. Desea pruebas y evita tener ciertas partes en la API pública: está bien, necesita dos puntos de entrada en su código con diferentes modificadores de acceso, así que aplique la parte 2. Everone puede elegir la solución que más le convenga.
Doc Brown
Claro que todos pueden elegir la solución que quieran, pero usted no entendió la pregunta si cree que quería probar el constructor. Eso es lo que intenté deshacerme.
Chris Wohlert