Pregunta

C #, nUnit y Rhino Mocks, si eso resulta ser aplicable.

Mi búsqueda con TDD continúa cuando intento ajustar las pruebas en torno a una función complicada. Digamos que estoy codificando un formulario que, cuando se guarda, también tiene que guardar objetos dependientes dentro del formulario ... respuestas para formar preguntas, adjuntos, si están disponibles, y " registro " entradas (como " blahblah actualizó el formulario. " o " blahblah adjuntó un archivo. "). Esta función de guardar también dispara correos electrónicos a varias personas, dependiendo de cómo haya cambiado el estado del formulario durante la función de guardar.

Esto significa que para probar completamente la función de guardado del formulario con todas sus dependencias, tengo que inyectar cinco o seis proveedores de datos para probar esta única función y asegurarme de que todo se dispare de la manera correcta y ordenada. Esto es incómodo al escribir los múltiples constructores encadenados para que el objeto de formulario inserte los proveedores simulados. Creo que me estoy perdiendo algo, ya sea en la manera de refactorizar o simplemente una mejor manera de configurar los proveedores de datos burlados.

¿Debería estudiar más a fondo los métodos de refactorización para ver cómo se puede simplificar esta función? ¿Cómo suena el patrón del observador, para que los objetos dependientes detecten cuándo se guarda el formulario principal y se manejan? Sé que la gente dice que se debe dividir la función para que se pueda probar ... lo que significa que pruebo las funciones de guardado individuales de cada objeto dependiente, pero no la función de guardado de la forma en sí misma, que dicta cómo debe guardarse cada una en el primer lugar?

¿Fue útil?

Solución

Usa un contenedor de AutoMocking. Hay uno escrito para RhinoMocks.

Imagina que tienes una clase con muchas dependencias inyectadas a través de la inyección del constructor. Esto es lo que parece configurarlo con RhinoMocks, sin contenedor de AutoMocking:

private MockRepository _mocks;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;
private IAddNewBroadcastEventBroker _addNewBroadcastEventBroker;
private IBroadcastService _broadcastService;
private IChannelService _channelService;
private IDeviceService _deviceService;
private IDialogFactory _dialogFactory;
private IMessageBoxService _messageBoxService;
private ITouchScreenService _touchScreenService;
private IDeviceBroadcastFactory _deviceBroadcastFactory;
private IFileBroadcastFactory _fileBroadcastFactory;
private IBroadcastServiceCallback _broadcastServiceCallback;
private IChannelServiceCallback _channelServiceCallback;

[SetUp]
public void SetUp()
{
    _mocks = new MockRepository();
    _view = _mocks.DynamicMock<IBroadcastListView>();

    _addNewBroadcastEventBroker = _mocks.DynamicMock<IAddNewBroadcastEventBroker>();

    _broadcastService = _mocks.DynamicMock<IBroadcastService>();
    _channelService = _mocks.DynamicMock<IChannelService>();
    _deviceService = _mocks.DynamicMock<IDeviceService>();
    _dialogFactory = _mocks.DynamicMock<IDialogFactory>();
    _messageBoxService = _mocks.DynamicMock<IMessageBoxService>();
    _touchScreenService = _mocks.DynamicMock<ITouchScreenService>();
    _deviceBroadcastFactory = _mocks.DynamicMock<IDeviceBroadcastFactory>();
    _fileBroadcastFactory = _mocks.DynamicMock<IFileBroadcastFactory>();
    _broadcastServiceCallback = _mocks.DynamicMock<IBroadcastServiceCallback>();
    _channelServiceCallback = _mocks.DynamicMock<IChannelServiceCallback>();


    _presenter = new BroadcastListViewPresenter(
        _addNewBroadcastEventBroker,
        _broadcastService,
        _channelService,
        _deviceService,
        _dialogFactory,
        _messageBoxService,
        _touchScreenService,
        _deviceBroadcastFactory,
        _fileBroadcastFactory,
        _broadcastServiceCallback,
        _channelServiceCallback);

    _presenter.View = _view;
}

Ahora, esto es lo mismo con un contenedor de AutoMocking:

private MockRepository _mocks;
private AutoMockingContainer _container;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;

[SetUp]
public void SetUp()
{

    _mocks = new MockRepository();
    _container = new AutoMockingContainer(_mocks);
    _container.Initialize();

    _view = _mocks.DynamicMock<IBroadcastListView>();
    _presenter = _container.Create<BroadcastListViewPresenter>();
    _presenter.View = _view;

}

Más fácil, ¿sí?

El contenedor de AutoMocking crea automáticamente simulacros para cada dependencia en el constructor, y puede acceder a ellos para realizar pruebas de este modo:

using (_mocks.Record())
    {
      _container.Get<IChannelService>().Expect(cs => cs.ChannelIsBroadcasting(channel)).Return(false);
      _container.Get<IBroadcastService>().Expect(bs => bs.Start(8));
    }

Espero que ayude. Sé que mi vida de prueba se ha simplificado mucho con la llegada del contenedor AutoMocking.

Otros consejos

Primero, si está siguiendo TDD, entonces no envuelve las pruebas alrededor de una función complicada. Envuelves la función alrededor de tus pruebas. En realidad, incluso eso no está bien. Usted entreteje sus pruebas y funciones, escribiendo ambas casi al mismo tiempo, con las pruebas un poco por delante de las funciones. Consulte Las tres leyes de TDD .

Cuando sigues estas tres leyes, y eres diligente en la refactorización, nunca terminas con "una función complicada". En lugar de eso, terminas con muchas funciones simples y probadas.

Ahora, a su punto. Si ya tienes " una función complicada " y desea ajustar las pruebas a su alrededor, entonces debería:

  1. Agrega tus mofas explícitamente, en lugar de hacerlo a través de DI. (por ejemplo, algo horrible como una bandera de "prueba" y una declaración "si" que selecciona las simulaciones en lugar de los objetos reales).
  2. Escriba algunas pruebas para cubrir el funcionamiento básico del componente.
  3. Refactorice sin piedad, dividiendo la función complicada en muchas funciones simples y pequeñas, mientras ejecuta las pruebas improvisadas con la mayor frecuencia posible.
  4. Presiona la bandera de 'prueba' lo más alto posible. A medida que se refactoriza, transfiera sus fuentes de datos a las funciones simples y pequeñas. No deje que la bandera de 'prueba' infecte a nadie más que a la función superior.
  5. Reescribe las pruebas. A medida que refactoriza, vuelva a escribir tantas pruebas como sea posible para llamar a las funciones pequeñas y simples en lugar de a la función grande de nivel superior. Puedes pasar tus simulacros a las funciones simples de tus pruebas.
  6. Deshazte de la bandera de 'prueba' y determina cuánto DI realmente necesitas. Ya que tiene pruebas escritas en los niveles inferiores que pueden insertar simulacros a través de documentos, probablemente ya no necesite burlarse de muchas fuentes de datos en el nivel superior.

Si, después de todo esto, el DI sigue siendo incómodo, piense en inyectar un solo objeto que contenga referencias a todas sus fuentes de datos. Siempre es más fácil inyectar una cosa que muchas.

Tienes razón en que puede ser engorroso.

El proponente de la metodología de simulacros señala que el código está escrito incorrectamente para estar con. Es decir, no debería estar construyendo objetos dependientes dentro de este método. Más bien, las API de inyección deberían tener funciones que creen los objetos apropiados.

En cuanto a burlarse de 6 objetos diferentes, eso es cierto. Sin embargo, si también estaba probando los sistemas de esos , esos objetos ya deberían tener una infraestructura de burla que pueda usar.

Finalmente, usa un marco de burla que haga parte del trabajo por ti.

No tengo su código, pero mi primera reacción es que su prueba está tratando de decirle que su objeto tiene demasiados colaboradores. En casos como este, siempre encuentro que hay una construcción faltante ahí que debe ser empaquetada en una estructura de nivel superior. El uso de un contenedor de automocking es solo una mezcla de los comentarios que obtiene de sus pruebas. Consulte http://www.mockobjects.com/2007/04 /test-smell-bloated-constructor.html para una discusión más larga.

En este contexto, generalmente encuentro declaraciones en la línea de " esto indica que su objeto tiene demasiadas dependencias " o " su objeto tiene demasiados colaboradores " Para ser un reclamo bastante especioso. Por supuesto, un controlador MVC o un formulario llamarán a muchos servicios y objetos diferentes para cumplir sus funciones; es, después de todo, sentado en la capa superior de la aplicación. Puede agrupar algunas de estas dependencias en objetos de nivel superior (por ejemplo, un ShippingMethodRepository y un TransitTimeCalculator se combinan en un ShippingRateFinder), pero esto solo va hasta ahora, especialmente para estos objetos de alto nivel orientados a la presentación. Es un objeto menos para burlarse, pero acaba de ofuscar las dependencias reales a través de una capa de indirección, en realidad no las eliminó.

Un consejo blasfemo es decir que si es dependiente inyectar un objeto y crear una interfaz para él, es poco probable que cambie alguna vez (¿realmente va a caer en un nuevo MessageBoxService mientras cambia su código? ¿De verdad? ), entonces no te molestes. Esa dependencia es parte del comportamiento esperado del objeto y solo debe probarlos juntos ya que la prueba de integración es donde se encuentra el valor real del negocio.

El otro consejo blasfemo es que generalmente veo poca utilidad en la prueba de unidades de los controladores MVC o Windows Forms. Cada vez que veo a alguien burlándose de HttpContext y haciendo pruebas para ver si se configuró una cookie, quiero gritar. ¿A quién le importa si el controlador de cuenta establece una cookie? Yo no. La cookie no tiene nada que ver con tratar el controlador como una caja negra; una prueba de integración es lo que se necesita para probar su funcionalidad (hmm, una llamada a PrivilegedArea () falló después de iniciar sesión () en la prueba de integración). De esta manera, evitará invalidar un millón de pruebas de unidades inútiles si el formato de la cookie de inicio de sesión cambia alguna vez.

Guarde las pruebas unitarias para el modelo de objetos, guarde las pruebas de integración para la capa de presentación y evite simular objetos cuando sea posible. Si burlarse de una dependencia en particular es difícil, es hora de ser pragmático: simplemente no haga la prueba de la unidad y en su lugar escriba una prueba de integración y deje de perder el tiempo.

La respuesta simple es que el código que está intentando probar está haciendo demasiado . Creo que seguir con el Principio de Responsabilidad Única podría ayudar.

El método del botón Guardar solo debe contener llamadas de nivel superior para delegar cosas a otros objetos . Estos objetos se pueden abstraer a través de interfaces. Luego, cuando prueba el método del botón Guardar, solo prueba la interacción con objetos simulados .

El siguiente paso es escribir pruebas en estas clases de nivel inferior, pero la cosa debería ser más fácil ya que solo las pruebas de forma aislada. Si necesita un código de configuración de prueba complejo, este es un buen indicador de un mal diseño (o un mal enfoque de prueba).

Lectura recomendada:

  1. Código limpio: Un manual de artesanía de software ágil
  2. Guía de Google para escribir código verificable

Constructor DI no es la única forma de hacer DI. Ya que está usando C #, si su constructor no realiza un trabajo significativo, podría usar la Propiedad DI. Eso simplifica enormemente las cosas en términos de los constructores de su objeto a costa de la complejidad de su función. Su función debe verificar la nulidad de las propiedades dependientes y lanzar InvalidOperation si son nulas, antes de que empiece a funcionar.

Cuando es difícil probar algo, generalmente es un síntoma de la calidad del código, que el código no es verificable (mencionado en este podcast , IIRC). La recomendación es refactorizar el código para que el código sea fácil de probar. Algunas heurísticas para decidir cómo dividir el código en clases son SRP y OCP . Para obtener instrucciones más específicas, sería necesario ver el código en cuestión.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top