26 votos

Unidad de pruebas de grandes bloques de código (asignaciones, traducción, etc)

Hemos unidad de prueba de la mayoría de nuestra lógica de negocio, pero están atrapados en la mejor manera de poner a prueba algunas de nuestras grandes tareas de los servicios y la importación/exportación de rutinas. Por ejemplo, considere la posibilidad de la exportación de datos de la nómina de un sistema a una 3ra parte del sistema. Para exportar los datos en el formato de las necesidades de la empresa, que necesita para golpear ~40 tablas, lo que crea una pesadilla de la situación para la creación de datos de prueba y la burla hacia fuera de las dependencias.

Por ejemplo, considere el siguiente (un subconjunto de ~3500 líneas de código de exportación):

public void ExportPaychecks()
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      WriteHeaderRow(pay);
      if (pay.IsFirstCheck)
      {
         WriteDetailRowType1(pay);
      }
   }
}

private void WriteHeaderRow(PayObject pay)
{
   //do lots more stuff
}

private void WriteDetailRowType1(PayObject pay)
{
   //do lots more stuff
}

Sólo se dispone de un método público en este particular, la exportación de la clase - ExportPaychecks(). Esa es realmente la única acción que tiene sentido para alguien que se hace llamar esta clase ... todo lo demás es privado (~80 funciones privadas). Podríamos hacerlos públicos para las pruebas, pero entonces tendríamos que burlarse de ellos para poner a prueba cada uno de ellos por separado (es decir, usted no puede probar ExportPaychecks en un vacío sin burlarse de la WriteHeaderRow función. Este es un gran dolor.

Dado que esta es una sola de exportación, por un solo proveedor, moviendo la lógica en el Dominio no tiene sentido. La lógica no tiene ningún dominio de significado fuera de esta clase en particular. Como prueba, se construye fuera de la unidad de pruebas que había cerca de 100% de cobertura de código ... pero esto requiere una cantidad increíble de datos de prueba escrito en el stub/simulacro de objetos, además de los más de 7000 líneas de código debido a la intermitencia/burlándose de nuestras dependencias.

Como un fabricante de HRIS software, tenemos cientos de exportaciones e importaciones. Hacer otras compañías de la unidad de probar este tipo de cosas? Si es así, ¿hay atajos para hacer que sea menos doloroso? Estoy a mitad de la tentación de decir que "no hay pruebas de unidad las rutinas importar/exportar" y limitarse a ejecutar las pruebas de integración más tarde.

Actualización - gracias por las respuestas de todos. Una cosa que me encantaría ver es un ejemplo, como todavía no estoy viendo cómo alguien puede convertirse en algo como un gran archivo de exportación en un lugar fácilmente comprobable bloque de código sin necesidad de encender el código en un lío.

13voto

Mark Seemann Puntos 102767

Este estilo de (intento de) la unidad de evaluación, donde se intenta cubrir toda una enorme base de código a través de un único método público siempre me recuerda a la de los cirujanos, dentistas o ginecólogos whe han de realizar operaciones complejas a través de pequeñas aberturas. Es posible, pero no fácil.

La encapsulación es un viejo concepto en el diseño orientado a objetos, pero algunas personas lo toman a tales extremos que la capacidad de prueba sufre. Hay otra OO principio llamado Abierto/Cerrado Principio que encaja mucho mejor con la capacidad de prueba. La encapsulación es muy valioso, pero no a expensas de la extensibilidad - de hecho, la capacidad de prueba de realidad es sólo otra palabra para el Abierto/Cerrado de Principio.

No estoy diciendo que usted debe hacer sus métodos privados público, pero lo que yo estoy diciendo es que usted debe considerar la refactorización su aplicación a la que se puede componer piezas - muchas pequeñas clases que colaboran en lugar de una de las grandes secuencias de Comandos de Transacción. Usted puede pensar que no tiene mucho sentido hacer esto de una solución a un solo proveedor, pero ahora mismo están sufriendo, y esta es una manera de salir.

Lo que va a suceder a menudo cuando se divide un único método en un complejo de la API es que también se obtiene una gran cantidad de flexibilidad. Lo que comenzó como un proyecto puede convertirse en una biblioteca reutilizable.


Aquí están algunas ideas sobre cómo realizar una refactorización para el problema en cuestión: Cada aplicación ETL debe realizar , al menos, estos tres pasos:

  1. Extraer los datos de la fuente
  2. Transformar los datos
  3. Cargar los datos en el destino

(de ahí el nombre ETL). Como punto de partida para la refactorización, esto nos da al menos tres clases con distintas responsabilidades: Extractor, Transformer y Loader. Ahora, en lugar de una gran clase, tiene tres con más específica responsabilidades. Nada desordenado sobre eso, y ya un poco más comprobables.

Ahora hacer zoom en cada una de estas tres áreas, y ver donde se pueden dividir las responsabilidades aún más.

  • Como mínimo, usted necesitará una buena representación en la memoria de cada "fila" de la fuente de datos. Si la fuente es una base de datos relacional, puede que desee utilizar un ORM, pero si no, las clases deben ser modelados de forma que se protege correctamente los invariantes de cada fila (por ejemplo, si un campo es que no aceptan valores null, la clase debe garantizar esta por lanzar una excepción si un valor null se intentó). Estas clases tienen un propósito bien definido y puede ser probado en forma aislada.
  • Lo mismo es cierto para el destino: Se necesita un buen modelo de objetos para que.
  • Si hay aplicaciones avanzadas filtrado en el lado que va a la fuente, usted podría considerar la implementación de estas mediante la Especificación de patrón de diseño. Aquellos que tienden a ser muy comprobable así.
  • La Transformación de paso es donde un montón de la acción sucede, pero ahora que usted tiene buenos modelos de objetos de origen y de destino, la transformación puede ser realizado por miembros de la comunidad - de nuevo comprobable clases.

Si usted tiene muchos 'filas' de origen y de destino de los datos, se puede dividir esta en Mappers para cada lógico 'fila', etc.

Nunca se debe llegar a ser desordenado, y con el beneficio añadido (además de la realización de pruebas automatizadas) es que el modelo de objetos es ahora más flexible. Si alguna vez necesita escribir otra aplicación ETL que implica uno de los dos lados, que ya tienen al menos un tercio del código escrito.

6voto

Wolfgang Puntos 1180

Algo general que vino a mi mente acerca de refactorización:

La refactorización no significa que usted tome su 3.5 k LOC y se divide en n partes. Yo no recomendaría a hacer algunas de sus 80 métodos públicos o cosas como esta. Es más como verticalmente rebanar el código:

  • Intente factor autónomo de algoritmos y estructuras de datos como los analizadores, los representadores, las operaciones de búsqueda, los convertidores especiales de estructuras de datos ...
  • Trate de averiguar si sus datos son procesados en varios pasos y se pueden construir en una especie de tubo y filtro de mecanismo, o arquitectura en capas. Trate de encontrar tantas capas como sea posible.
  • Técnica independiente (archivos, base de datos) de las piezas de partes lógicas.
  • Si usted tiene muchos de estos importación/exportación de los monstruos ver lo que tienen en común y el factor de que las partes y la reutilización de los mismos.
  • Espera en general que el código es demasiado densa, es decir, contiene demasiados diferentes funcionalidades junto a cada uno en muy pocos LOC. Visitar los diferentes "invenciones" en el código y piensa si en realidad son complicadas instalaciones que son vale la pena tener su propia clase(s).
    • Ambos LOC y el número de clases son propensos a aumentar cuando refactorizar.
    • Trate de hacer que su código real simple ('bebé código') dentro de las clases y compleja en las relaciones entre las clases.

Como resultado, usted no tiene que escribir las pruebas unitarias que cubren la totalidad del 3,5 k LOC. Sólo pequeñas fracciones de la cubierta en una sola prueba, y tendrás muchas pequeñas pruebas que son independientes el uno del otro.


EDITAR

Aquí tienes una buena lista de patrones de refactorización. Entre ellas, una muestra bastante bien mi intención: Descomponer Condicional.

En el ejemplo, ciertas expresiones se deja fuera a los métodos. No sólo hace el código más fácil de leer, pero también lograr la oportunidad a prueba la unidad de todos los métodos.

Incluso mejor, usted puede levantar este patrón a un nivel más alto y el factor de esas expresiones, los algoritmos, los valores, etc. no sólo a los métodos, sino también a sus propias clases.

5voto

Burt Puntos 4051

Lo que usted debe tener inicialmente son las pruebas de integración. Estos se prueba que las funciones se comportan como se esperaba y que podría golpear la base de datos real para ello.

Una vez que usted tiene que savety neto podría empezar a refactorizar el código para que sea más fácil de mantener y la introducción de pruebas de unidad.

Como se ha mencionado por serbrech Workign de manera Efectiva con el código Heredado ayudará a usted a más no poder, yo les recomiendo vivamente la lectura de ella, incluso para proyectos de nueva construcción.

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

La principal pregunta que yo haría es ¿con qué frecuencia el cambio de código? Si es poco frecuente que es lo que realmente vale la pena el esfuerzo tratando de introducir la unidad de pruebas, si es que cambian con frecuencia, a continuación, me gustaría definitivamente considere la posibilidad de limpiarlo un poco.

3voto

Frank Schwieterman Puntos 13519

Suena como pruebas de integración puede ser suficiente. Especialmente si estas rutinas de exportación que no cambian una vez que su hecho o son utilizados solamente por un tiempo limitado. Acaba de obtener algunos ejemplos de los datos de entrada con una de las variaciones, y tener una prueba que verifica el resultado final es el esperado.

Una preocupación con las pruebas fue la cantidad de falsos los datos que tenía a crear. Usted puede ser capaz de reducir mediante la creación de una compartido luminaria (http://xunitpatterns.com/Shared%20Fixture.html). Para las pruebas de unidad el accesorio que puede ser una representación en la memoria de objetos de negocio para la exportación, o para el caso de las pruebas de integración puede ser las bases de datos reales que se inicializa con los datos conocidos. El punto es que, sin embargo, se genera la compartida accesorio es el mismo en cada prueba, por lo que la creación de nuevas pruebas es sólo una cuestión de hacer ajustes menores a los existentes en la luminaria para activar el código que desea probar.

Así que usted debe utilizar las pruebas de integración? Una de las barreras es cómo configurar el compartido de la luminaria. Si usted puede duplicar las bases de datos en alguna parte, usted podría usar algo como DbUnit para preparar el compartido de la luminaria. Podría ser más fácil para romper el código en trozos (importación, transformación, exportación). A continuación, utilice el DbUnit a base de pruebas para probar la importación y la exportación, y el uso regular de la unidad de pruebas para comprobar la transformación de paso. Si usted hace que usted no necesita DbUnit para configurar un compartida accesorio para la transformación de paso. Si se puede romper el código en 3 pasos (extracción, transformación, exportación) al menos se puede enfocar sus esfuerzos de prueba en la parte eso es probable que tenga errores o cambiar más tarde.

2voto

Tomasz Zielinski Puntos 9300

Yo no tengo nada que ver con C#, pero tengo una idea que podría tratar aquí. Si usted divide su código un poco, entonces te darás cuenta de que lo que tienes es básicamente la cadena de operaciones que se realizan en las secuencias.

Primero uno se paga la fecha actual:

    var pays = _pays.GetPaysForCurrentDate();

Segundo incondicionalmente el resultado de los procesos de

    foreach (PayObject pay in pays)
    {
       WriteHeaderRow(pay);
    }

Tercero realiza el procesamiento condicional:

    foreach (PayObject pay in pays)
    {
       if (pay.IsFirstCheck)
       {
          WriteDetailRowType1(pay);
       }
    }

Ahora, usted podría hacer una de las etapas más genérico (lo siento por el pseudocódigo, no sé C#):

    var all_pays = _pays.GetAll();

    var pwcdate = filter_pays(all_pays, current_date()) // filter_pays could also be made more generic, able to filter any sequence

    var pwcdate_ann =  annotate_with_header_row(pwcdate);       

    var pwcdate_ann_fc =  filter_first_check_only(pwcdate_annotated);  

    var pwcdate_ann_fc_ann =  annotate_with_detail_row(pwcdate_ann_fc);   // this could be made more generic, able to annotate with arbitrary row passed as parameter

    (Etc.)

Como se puede ver, ahora se han puesto de desvinculada de las etapas que podrían ser probado por separado y, a continuación, conectados entre sí en orden arbitrario. Este tipo de conexión, o de la composición, también podría ser probados por separado. Y así sucesivamente (es decir, - usted puede elegir lo que a la prueba)

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by: