2010-11-10 16 views
7

suponga una clase legado y la estructura método como el de abajoPrueba de la unidad y la inyección de dependencia con dependencias anidadas

public class Foo 
{ 
    public void Frob(int a, int b) 
    { 
     if (a == 1) 
     { 
      if (b == 1) 
      { 
       // does something 
      } 
      else 
      { 
       if (b == 2) 
       { 
        Bar bar = new Bar(); 
        bar.Blah(a, b); 
       } 
      } 
     } 
     else 
     { 
      // does something 
     } 
    } 
} 

public class Bar 
{ 
    public void Blah(int a, int b) 
    { 
     if (a == 0) 
     { 
      // does something 
     } 
     else 
     { 
      if (b == 0) 
      { 
       // does something 
      } 
      else 
      { 
       Baz baz = new Baz(); 
       baz.Save(a, b); 
      } 
     } 
    } 
} 

public class Baz 
{ 
    public void Save(int a, int b) 
    { 
     // saves data to file, database, whatever 
    } 
} 

Y entonces asumir la gestión emite un mandato nebulosa para realizar las pruebas unitarias para cada nueva cosa que hacemos, ya sea una función adicional, requisito modificado o corrección de errores.

Puedo ser un riguroso para la interpretación literal, pero creo que la frase "prueba de unidad" significa algo. Esto no significa, por ejemplo, que las entradas dadas de 1 y 2 que la prueba unitaria de Foo.Frob deberían tener éxito solo si 1 y 2 se guardan en una base de datos. De acuerdo con lo que he leído, creo que en última instancia significa basado en entradas de 1 y 2, Frob invocado Bar.Blah. Si o no Bar.Blah hizo lo que se supone que debe hacer, no es mi preocupación inmediata. Si me preocupa probar todo el proceso, creo que hay otro término para eso, ¿no? Pruebas funcionales? ¿Prueba de escenario? Lo que sea. Corrígeme si estoy siendo demasiado rígido, por favor!

Siguiendo con mi interpretación rígida, por el momento, vamos a suponer que quiero tratar de utilizar la inyección de dependencia, con uno de los beneficios es que puedo burlarse de distancia mis clases para que pueda, por ejemplo, no persistir mi datos de prueba a una base de datos o archivo o cualquiera que sea el caso. En este caso, Foo.Frob necesita IBar, IBar necesita IBaz, IBaz puede necesitar una base de datos. ¿Dónde se inyectarán estas dependencias? En Foo? O ¿necesita Foo simplemente IBar, y luego Foo es responsable de crear una instancia de IBaz?

Cuando entra en una estructura anidada como esta, puede ver rápidamente que puede haber varias dependencias necesarias. ¿Cuál es el método preferido o aceptado para realizar tal inyección?

Respuesta

7

Comencemos con su última pregunta. ¿Dónde se inyectan las dependencias? Un enfoque común es usar inyección de constructor (como described by Fowler). Entonces, Foo se inyecta con un IBar en el constructor. La implementación concreta de IBar, Bar a su vez tiene un IBaz inyectado en su constructor. Y finalmente, la implementación IBaz (Baz) tiene un IDatabase (o lo que sea) inyectado. Si usa un marco DI como Castle Project, simplemente le pedirá al contenedor DI que resuelva una instancia de Foo por usted. A continuación, utilizará lo que haya configurado para determinar qué implementación de IBar está utilizando. Si se determina que su aplicación es de IBarBar será entonces determinar qué implementación de IBaz que está utilizando, etc.

Lo que este enfoque le da, es que se puede probar cada una de las implementaciones concretas de forma aislada, y sólo comprobar que invoca la abstracción (burlada) correctamente.

Para comentar sobre sus preocupaciones acerca de ser demasiado rígido, etc., lo único que puedo decir es que, en mi opinión, está eligiendo el camino correcto. Dicho esto, la administración podría estar sorprendida cuando el costo real de implementar todas esas pruebas se vuelva aparente para ellos.

Espero que esto ayude.

+0

+1 - para dependencias profundamente anidadas, creo que los marcos IoC/DI Container a menudo son una buena solución. Ver http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx – TrueWill

2

No creo que haya un método "preferido" para abordar esto, pero una de sus principales preocupaciones parece ser que con la inyección de dependencia, cuando crea Foo, también necesita crear Baz que podría ser innecesario. Una forma simple de evitar esto es Bar para no depender directamente de IBaz pero en un Lazy<IBaz> o Func<IBaz>, permitiendo que su contenedor IoC cree una instancia de Bar sin crear inmediatamente Baz.

Por ejemplo:

public interface IBar 
{ 
    void Blah(int a, int b); 
} 

public interface IBaz 
{ 
    void Save(int a, int b); 
} 

public class Foo 
{ 
    Func<IBar> getBar; 
    public Foo(Func<IBar> getBar) 
    { 
     this.getBar = getBar; 
    } 

    public void Frob(int a, int b) 
    { 
     if (a == 1) 
     { 
      if (b == 1) 
      { 
       // does something 
      } 
      else 
      { 
       if (b == 2) 
       {       
        getBar().Blah(a, b); 
       } 
      } 
     } 
     else 
     { 
      // does something 
     } 
    } 
} 



public class Bar : IBar 
{ 
    Func<IBaz> getBaz; 

    public Bar(Func<IBaz> getBaz) 
    { 
     this.getBaz = getBaz; 
    } 

    public void Blah(int a, int b) 
    { 
     if (a == 0) 
     { 
      // does something 
     } 
     else 
     { 
      if (b == 0) 
      { 
       // does something 
      } 
      else 
      { 
       getBaz().Save(a, b); 
      } 
     } 
    } 
} 

public class Baz: IBaz 
{ 
    public void Save(int a, int b) 
    { 
     // saves data to file, database, whatever 
    } 
} 
1

yo diría que tiene razón de las pruebas unitarias, que debe cubrir un bastante pequeño 'unidad' de código, aunque exactamente cuánto es tema de debate. Sin embargo, si toca la base de datos, es casi seguro que no es una prueba unitaria, lo llamaría una prueba de integración.

Por supuesto, podría ser que la "gestión" realmente no se preocupe por tales cosas y estaría muy contento con las pruebas de integración. Todavía son pruebas perfectamente válidas, y probablemente sean más fáciles de agregar, aunque no necesariamente conducen a un mejor diseño, como lo suelen hacer las pruebas unitarias.

Pero sí, inyecte su IBaz en su IBar cuando se cree, e inyecte su IBar en su Foo. Esto se puede hacer en el constructor o un setter. Constructor es (IMO) mejor ya que solo genera objetos válidos. Una opción que puede hacer (conocida como DI del hombre pobre) es sobrecargar el constructor, de modo que puede pasar un IBar para probar y crear una barra en el constructor sin parámetros utilizado en el código. Pierdes los buenos beneficios de diseño, pero vale la pena considerarlo.

Cuando usted ha trabajado todo eso, tratar un contenedor IoC como Ninject, lo que puede hacer su vida más fácil.

(También considere herramientas como TypeMock o Moles, que pueden simular cosas sin una interfaz, pero tenga en cuenta que es una trampa y no obtendrá un diseño mejorado, por lo que debería ser un último recurso).

0

cuando se tienen problemas con una jerarquía anidada sólo significa que no se está inyectando suficientes dependencias.

El problema aquí es que tenemos Baz y parece que se necesita pasar a Baz Foo quien lo pasa al bar que finalmente se llama a un método en él. Este parece ser un montón de trabajo y un poco inútil ...

Lo que debe hacer es pasar Baz como el parámetro del objeto constructor bar. Bar debe pasar al constructor del objeto Foo. Foo nunca debería tocar ni siquiera saber sobre la existencia de Baz. Solo Bar se preocupa por Baz. Al probar Foo, usaría otra implementación de la interfaz Bar. Esta implementación probablemente no hace más que registrar el hecho de que Blah fue llamado. No necesita considerar la existencia de Baz.

Usted está pensando probablemente algo como esto:

class Foo 
{ 
    Foo(Baz baz) 
    { 
     bar = new Bar(baz); 
    } 

    Frob() 
    { 
     bar.Blah() 
    } 
} 

class Bar 
{ 
    Bar(Baz baz); 

    void blah() 
    { 
      baz.biz(); 
    } 
} 

que debe hacer algo como esto:

class Foo 
{ 
    Foo(Bar bar); 

    Frob() 
    { 
     bar.Blah() 
    } 
} 

class Bar 
{ 
    Bar(Baz baz); 

    void blah() 
    { 
      baz.biz(); 
    } 
} 

Si lo haces correctamente, cada objeto sólo tendrá que hacer frente a los objetos que interactúa directamente con.

En su código real, que la construcción de los objetos sobre la marcha.Para hacer eso, solo tendrá que pasar instancias de BarFactory y BazFactory para construir los objetos cuando sea necesario. El principio básico sigue siendo el mismo.

0

Suena como si hubiera un poco de lucha aquí para:

  1. acuerdo con el código heredado
  2. continúan escribiendo código mantenible
  3. prueba de su código (continuación de 2 en realidad)
  4. y también , Supongo, liberar algo

El uso de la administración de "pruebas unitarias" solo puede determinarse preguntándoles, pero ella e es mi 2c sobre lo que podría ser una buena idea aquí con respecto a los 4 temas anteriores.

Creo que es importante probar que Frob invocó Bar.Blah y que Bar.Blah hizo lo que se supone que debe hacer. De acuerdo, estas son pruebas diferentes, pero para lanzar un software libre de errores (o como pocos errores), realmente necesita tener pruebas unitarias (Frob invocado Bar.Blah), así como pruebas de integración (Bar.Blah hizo lo que es supone que debe hacer). Sería genial si pudiera probar la unidad Bar.Blah también, pero si no espera que cambie, puede que no sea demasiado útil.

Sin duda, en el futuro querrá agregar pruebas unitarias cada vez que encuentre un error, preferiblemente antes de solucionarlo. De esta forma, puede asegurarse de que la prueba se interrumpe antes de la reparación y luego la corrección hace que pase la prueba.

No desea pasar todo el día refabricando o reescribiendo su código base, por lo que debe ser prudente en la forma de tratar con las dependencias. En el ejemplo que proporcionó, dentro de Foo, sería mejor que promoviera Bar a internal property y configurara el proyecto para hacer que las partes internas sean visibles para su proyecto de prueba (utilizando el atributo InternalsVisibleTo en AssemblyInfo.cs). El constructor predeterminado de Bar podría establecer la propiedad en new Bar(). Su prueba puede establecerlo en alguna subclase de Bar utilizada para la prueba. O un talón. Creo que eso reducirá la cantidad de cambios que tendrá que hacer para que esto sea comprobable en el futuro.

Y, por supuesto, no necesita hacer ninguna refacturación de una clase hasta que realice algunos otros cambios a esa clase.

2

tipo de prueba que describió en la primera parte de su publicación (cuando prueba todas las partes juntas) generalmente se define como prueba de integración. Como una buena práctica en su solución, debe tener un proyecto de prueba de unidad y un proyecto de prueba de integración. Para inyectar dependencias en su código, la primera y más importante regla es codificar usando interfaces. Suponiendo esto, supongamos que su clase contiene una interfaz como miembro y desea inyectarla/simularla: puede exponerla como propiedad o pasar la implementación utilizando el constructor de la clase. Prefiero usar propiedades para exponer dependencias, de esta manera el constructor no se vuelve demasiado detallado. que sugerimos utilizar NUnit o MbUnit como un marco de pruebas y Moq como un marco de burla (más clara en ella de salidas de Rhino se burla) Aquí está la documentación con algunos ejemplos sobre la forma de burlarse con Moq http://code.google.com/p/moq/wiki/QuickStart

espero que ayuda

+0

Debo decir que nunca tuve el problema de que los constructores fueran demasiado detallados. Casi nunca tengo más de cuatro dependencias en una clase. Quizás tus clases estén haciendo demasiado y necesites refactorizar. Además, poner las dependencias en el constructor hace que las dependencias sean muy claras. – Steven

+0

+1 para poner pruebas de integración en su propio proyecto. – Steven

Cuestiones relacionadas