2011-06-07 6 views
6

He estado haciendo mi primer proyecto de desarrollo impulsado por prueba recientemente y he estado aprendiendo Ninject y MOQ. Este es mi primer intento de todo esto. Descubrí que el enfoque TDD ha sido estimulante, y Ninject y MOQ han sido geniales. El proyecto en el que estoy trabajando no ha sido particularmente el mejor para Ninject ya que es un programa C# altamente configurable que está diseñado para probar el uso de una interfaz de servicio web.Cómo probar Ninject ConstructorArguments utilizando objetos MOQ?

Lo he dividido en módulos y tengo interfaces en toda la tienda, pero todavía estoy descubriendo que tengo que usar muchos argumentos de constructor cuando obtengo una implementación de un servicio desde el núcleo de Ninject. Por ejemplo;

En mi módulo Ninject;

Bind<IDirEnum>().To<DirEnum>() 

Mi DirEnum class;

public class DirEnum : IDirEnum 
{ 
    public DirEnum(string filePath, string fileFilter, 
     bool includeSubDirs) 
    { 
     .... 

En mi clase Configurator (este es el punto de entrada principal) que engancha todos los servicios;

class Configurator 
{ 

    public ConfigureServices(string[] args) 
    { 
     ArgParser argParser = new ArgParser(args); 
     IDirEnum dirEnum = kernel.Get<IDirEnum>(
      new ConstructorArgument("filePath", argParser.filePath), 
      new ConstructorArgument("fileFilter", argParser.fileFilter), 
      new ConstructorArgument("includeSubDirs", argParser.subDirs) 
     ); 

filePath, fileFilter e includeSubDirs son opciones de línea de comandos para el programa. Hasta aquí todo bien. Sin embargo, como soy un tipo concienzudo, tengo una prueba que cubre este código. Me gustaría usar un objeto MOQ. He creado un módulo Ninject para mis pruebas;

public class TestNinjectModule : NinjectModule 
{ 
    internal IDirEnum mockDirEnum {set;get}; 
    Bind<IDirEnum>().ToConstant(mockDirEnum); 
} 

Y en mi prueba lo uso así;

[TestMethod] 
public void Test() 
{ 
    // Arrange 
    TestNinjectModule testmodule = new TestNinjectModule(); 
    Mock<IDirEnum> mockDirEnum = new Mock<IDirEnum>(); 
    testModule.mockDirEnum = mockDirEnum; 
    // Act 
    Configurator configurator = new Configurator(); 
    configurator.ConfigureServices(); 
    // Assert 

    here lies my problem! How do I test what values were passed to the 
    constructor arguments??? 

Así que lo anterior muestra mi problema. ¿Cómo puedo probar qué argumentos pasaron a los ConstructorArguments del objeto simulado? ¿Adivino que Ninject está prescindiendo de los Argumentos del Constuctor en este caso ya que el Enlazador no los requiere? ¿Puedo probar esto con un objeto MOQ o necesito codificar manualmente un objeto simulado que implemente DirEnum y acepte y 'registre' los argumentos del constructor?

n.b. este código es código 'de ejemplo', es decir, no he reproducido mi código al pie de la letra, pero creo que he expresado lo suficiente como para transmitir los problemas. Si necesita más contexto, ¡por favor pregunte!

Gracias por mirar. Sea amable, esta es mi primera vez ;-)

Jim

+4

¿Qué hace DirEnum? Mi regla general es tomar dependencias de "tiempo de compilación" a través de parámetros de constructor y dependencias de "tiempo de ejecución" a través de parámetros de método. Como 'filePath', 'fileFilter' y 'includeSubDirs' son argumentos de línea de comandos, los considero como una dependencia de "tiempo de ejecución" y, por lo tanto, deben pasarse como parámetros de método al método que los necesita. – mrydengren

+0

@mrydengren: me gusta el sonido de eso: "tiempo de compilación para el constructor y tiempo de ejecución para los parámetros del método". – Steven

Respuesta

15

Hay algunos problemas con la forma en que ha diseñado su aplicación. En primer lugar, está llamando al kernel Ninject directamente desde dentro de su código. Esto se llama Service Locator pattern y it is considered an anti-pattern. Hace que probar tu aplicación sea mucho más difícil y ya estás experimentando esto. Está intentando burlarse del contenedor de Ninject en su prueba de unidad, lo que complica enormemente las cosas.

A continuación, está inyectando tipos primitivos (string, bool) en el constructor de su tipo DirEnum.Me gusta cómo MNrydengren estados en los comentarios:

toma "tiempo de compilación" dependencias través de parámetros del constructor y "run-time" dependencias a través del método parámetros

Es difícil para mí adivina qué debería hacer esa clase, pero como estás inyectando estas variables que cambian en tiempo de ejecución en el constructor DirEnum, terminas con una aplicación difícil de probar.

Existen varias formas de arreglar esto. Dos que vienen en mente son el uso de inyección de método y el uso de una fábrica. Cuál es factible depende de usted.

El uso de la inyección método, la clase Configurator se verá así:

class Configurator 
{ 
    private readonly IDirEnum dirEnum; 

    // Injecting IDirEnum through the constructor 
    public Configurator(IDirEnum dirEnum) 
    { 
     this.dirEnum = dirEnum; 
    } 

    public ConfigureServices(string[] args) 
    { 
     var parser = new ArgParser(args); 

     // Inject the arguments into a method 
     this.dirEnum.SomeOperation(
      argParser.filePath 
      argParser.fileFilter 
      argParser.subDirs); 
    } 
} 

El uso de una fábrica, lo que se necesita para definir una fábrica que sabe cómo crear nuevos IDirEnum tipos:

interface IDirEnumFactory 
{ 
    IDirEnum CreateDirEnum(string filePath, string fileFilter, 
     bool includeSubDirs); 
} 

Su clase Configuration ahora puede depender de la interfaz IDirEnumFactory:

class Configurator 
{ 
    private readonly IDirEnumFactory dirFactory; 

    // Injecting the factory through the constructor 
    public Configurator(IDirEnumFactory dirFactory) 
    { 
     this.dirFactory = dirFactory; 
    } 

    public ConfigureServices(string[] args) 
    { 
     var parser = new ArgParser(args); 

     // Creating a new IDirEnum using the factory 
     var dirEnum = this.dirFactory.CreateDirEnum(
      parser.filePath 
      parser.fileFilter 
      parser.subDirs); 
    } 
} 

Vea cómo en ambos ejemplos las dependencias se inyectan en la clase Configurator. Esto se llama Dependency Injection pattern, opuesto al patrón de Localizador de servicios, donde Configurator pregunta por sus dependencias llamando al kernel de Ninject.

Ahora, dado que su Configurator está completamente libre de cualquier contenedor IoC, puede probar fácilmente esta clase, inyectando una versión simulada de la dependencia que espera.

Lo que queda es configurar el contenedor Ninject en la parte superior de la aplicación (en la terminología DI: composition root). Con el ejemplo de la inyección método, la configuración del contenedor permanecería igual, con el ejemplo de la fábrica, que tendrá que reemplazar la línea Bind<IDirEnum>().To<DirEnum>() con algo de la siguiente manera:

public static void Bootstrap() 
{ 
    kernel.Bind<IDirEnumFactory>().To<DirEnumFactory>(); 
} 

Por supuesto, usted tendrá que crear el DirEnumFactory:

class DirEnumFactory : IDirEnumFactory 
{ 
    IDirEnum CreateDirEnum(string filePath, string fileFilter, 
     bool includeSubDirs) 
    { 
     return new DirEnum(filePath, fileFilter, includeSubDirs); 
    }   
} 

ADVERTENCIA: Ten en cuenta que la fábrica abstracciones son en la mayoría de los casos no es el mejor diseño, como se explica here.

Lo último que debe hacer es crear una nueva instancia de Configurator. Simplemente puede hacer esto de la siguiente manera:

public static Configurator CreateConfigurator() 
{ 
    return kernel.Get<Configurator>(); 
} 

public static void Main(string[] args) 
{ 
    Bootstrap(): 
    var configurator = CreateConfigurator(); 

    configurator.ConfigureServices(args); 
} 

Aquí lo llamamos el núcleo. Aunque se debe evitar llamar al contenedor directamente, siempre habrá al menos un lugar en su aplicación donde llame al contenedor, simplemente porque debe cablear todo. Sin embargo, tratamos de minimizar la cantidad de veces que se llama directamente al contenedor, porque mejora, entre otras cosas, la capacidad de prueba de nuestro código.

Vea cómo realmente no respondí su pregunta, pero le mostré una manera de solucionar el problema de manera muy efectiva.

Es posible que desee probar su configuración DI. Esa es una OMI muy válida. Lo hago en mis aplicaciones. Pero para esto, a menudo no necesita el contenedor DI, o incluso si lo hace, esto no significa que todas sus pruebas deben tener una dependencia en el contenedor. Esta relación solo debería existir para las pruebas que prueban la configuración DI. Aquí está una prueba:

[TestMethod] 
public void DependencyConfiguration_IsConfiguredCorrectly() 
{ 
    // Arrange 
    Program.Bootstrap(); 

    // Act 
    var configurator = Program.CreateConfigurator(); 

    // Assert 
    Assert.IsNotNull(configurator); 
} 

este examen depende indirectamente de la Ninject y se producirá un error cuando Ninject no es capaz de construir una nueva instancia Configurator. Cuando mantiene sus constructores limpios de cualquier lógica y solo los usa para almacenar las dependencias tomadas en campos privados, puede ejecutar esto, sin el riesgo de llamar a una base de datos, servicio web o cualquier otra cosa.

Espero que esto ayude.

+0

Gracias @Steven. Ahora he vuelto a factorizar, y tengo unas buenas fábricas que se inyectan. De esta forma, estoy configurando todas las dependencias de servicio estáticas y los servicios que puedo o no requerir según la configuración de tiempo de ejecución que obtengo de las fábricas. Esto ha significado pruebas unitarias mucho más fáciles, y ahora no necesito pasar un núcleo de Ninject a ninguna prueba, aparte de la prueba de raíz donde pruebo la lógica del método que obtiene un IConfigurator del kernel. ¡Muchas gracias por tomarse el tiempo para responder tan rápido! Mucho respeto :-) – JBowen

+0

La prueba de la unidad no debe depender del contenedor DI. En ese caso, ¿debería usar burlas para simular todas las dependencias? – Dariusz

+0

@Dario: De hecho, no quiere que ninguna referencia a ningún contenedor de IoC en sus pruebas sea la que sea; esperar, por supuesto, las pruebas que verifican la configuración DI en sí. Para probar una clase de forma aislada (eso es lo que es la prueba unitaria), debe proporcionar dependencias falsas (o pasar 'null' cuando la dependencia no se usa en esa prueba en particular). Si son objetos simulados o no depende de lo que quiere probar. Con un diseño de aplicación limpio y pruebas de unidades limpias, casi nunca necesitará un marco de burla. Además, los marcos burlones contaminan las pruebas unitarias con detalles técnicos. – Steven

Cuestiones relacionadas