2008-09-15 16 views
16

Acabo de comenzar a escribir pruebas unitarias para un módulo de código heredado con grandes dependencias físicas utilizando la directiva #include. He estado lidiando con ellos de algunas maneras que me parecieron excesivamente tediosas (proporcionar encabezados vacíos para romper largas listas de #include de dependencia, y #define para evitar que se compilen las clases) y estaba buscando algunas estrategias mejores para manejar estos problemas.C++ Unit Testing Legacy Code: cómo manejar #include?

He estado topando frecuentemente con el problema de duplicar casi todos los archivos de cabecera con una versión en blanco para separar la clase que estoy probando en su totalidad, y luego escribir código stub/falsa/falso sustancial para objetos que tendrá que ser reemplazado ya que ahora están indefinidos.

¿Alguien conoce algunas mejores prácticas?

+0

Ojalá pudiera este voto positivo x 10, esta es, de lejos, la mejor pregunta sobre SO que he leído hasta ahora. He perdido la cuenta de las personas que simplemente se rinden 'No podemos probar ... tenemos código C++ .. ¡Boohoo!' – Gishu

+0

Gishu, no podría estar más de acuerdo. Estoy teniendo las mismas discusiones aquí donde trabajo. – Lou

Respuesta

9

La depresión en las respuestas es abrumadora ... Pero no temas, tenemos the holy book to exorcise the demons of legacy C++ code. En serio, solo compre el libro si está en línea durante más de una semana de justa con el código de C++ heredado.

Pasar a la página 127: El caso de las horribles incluyen dependencias. (Ahora estoy ni siquiera dentro de miles de plumas de Michael, pero aquí como corta-as-I-podría-Gestión respuesta ..)

Problema: En C++ si un classA necesita saber sobre ClassB, Clase B de la declaración se levanta directamente/se incluye textualmente en el archivo fuente de la Clase A. Y dado que a los programadores nos encanta llevarlo al extremo equivocado, un archivo puede incluir recursivamente un trillón de otros transitoriamente. Las construcciones llevan años ... pero bueno, al menos se construye ... podemos esperar.

Ahora decir 'instanciar ClassA bajo un arnés de prueba es difícil' es insuficiente. (Citando el ejemplo de MF - Scheduler es nuestro hijo poster problema con deps en abundancia.)

#include "TestHarness.h" 
#include "Scheduler.h" 
TEST(create, Scheduler)  // your fave C++ test framework macro 
{ 
    Scheduler scheduler("fred"); 
} 

Esto incluye llevar a cabo el dragón con una ráfaga de errores de generación.
Golpe n. ° 1 Paciencia-n-Persistencia: Tome en cada uno uno a la vez y decida si realmente necesitamos esa dependencia. Supongamos que SchedulerDisplay es uno de ellos, cuyo método displayEntry se llama en el ctor de Scheduler.
Blow # 2 falso-que-hasta-que-hacer-que (Gracias RonJ):

#include "TestHarness.h" 
#include "Scheduler.h" 
void SchedulerDisplay::displayEntry(const string& entryDescription) {} 
TEST(create, Scheduler) 
{ 
    Scheduler scheduler("fred"); 
} 

y pop va la dependencia y toda su transitiva incluye. También puede reutilizar los métodos falsos al encapsularlo en un archivo Fakes.h para incluirlo en los archivos de prueba.
Golpe # 3 Práctica: Puede que no sea siempre así de simple ... pero ya entiendes. Después de los primeros duelos, el proceso de ruptura deps obtendrá Easy-N-mecánica

Advertencias (¿Mencioné que hay advertencias? :)

  • necesitamos una estructura separada para casos de prueba en Este archivo ; solo podemos tener 1 definición para el método SchedulerDisplay :: displayEntry en un programa.Por lo tanto, cree un programa separado para las pruebas del planificador.
  • No estamos rompiendo ninguna dependencia en el programa, por lo que no estamos limpiando el código.
  • Necesita mantener esas falsificaciones mientras necesitemos las pruebas.
  • Su sentido de la estética puede ser ofendido por un tiempo .. sólo muerden el labio y 'oso con nosotros para un mejor mañana'

utilizar esta técnica para un muy gran clase con graves problemas de dependencia. No lo use a menudo o ligeramente. Use esto como punto de partida para refactorizaciones más profundas. Con el tiempo, este programa de prueba puede tomarse detrás del establo a medida que extrae más clases (CON sus propias pruebas).

Para obtener más información, lea el libro. Inestimable. ¡Lucha contra hermano!

+0

Aunque considero que esta es una respuesta aceptable, creo que realmente pasa por alto el proceso entre el suministro de stubs falsos en la implementación de funciones alternativas y la magia que debe preformarse durante el proceso de compilación. – MasD

1

Desde que está probando el código heredado Estoy asumiendo que no se puede perfeccionar por dicho código a tener menos dependencias (por ejemplo, mediante el uso de la pimpl idiom)

Eso te deja con pocas opciones me temo. Cada encabezado que se incluyó para un tipo o función necesitará un objeto de simulacro para ese tipo o función para compilar todo, hay poco que puede hacer ...

0

Si sigue escribiendo stubs/mock/fake code, corre el riesgo de hacerlo pruebas unitarias en una clase que tiene un comportamiento diferente luego cuando se compila en el proyecto principal.

Pero si esas inclusiones están ahí y no tienen ningún comportamiento adicional, entonces está bien.

Intentaré no cambiar nada en las inclusiones mientras realizo las pruebas unitarias, así que está seguro (hasta donde puede llegar con el código heredado :)) de que está probando el código real.

1

No estoy respondiendo su pregunta directamente, pero me temo que las pruebas unitarias simplemente no son lo que hay que hacer si trabaja con grandes cantidades de código heredado.

Después de liderar un equipo de XP en un proyecto de desarrollo de campo verde, realmente me encantaron mis pruebas unitarias. Las cosas sucedieron y unos años más tarde me encuentro trabajando en una gran base de código heredado que tiene muchos problemas de calidad.

He intentado encontrar una manera de agregar pruebas de unidades para la aplicación, pero al final solo quedó atascado en una trampa-22:

  1. el fin de escribir lo que significa unidad completa pone a prueba el código tendría que ser refactorizado
  2. Sin pruebas unitarias será demasiado peligroso refactorizar el código.

Si te sientes como un héroe y bebes la genialidad en las pruebas unitarias, entonces aún puedes intentarlo pero existe el riesgo real de que termines con solo más código de prueba de poco valor que ahora también necesita ser mantenido.

A veces es mejor trabajar en el código de la manera que está "diseñado" para trabajar.

0

Definitivamente estás entre un rock y un lugar difícil con un código heredado con grandes dependencias. Tienes mucho trabajo duro por delante para resolverlo todo.

Por lo que dices, parece que estás tratando de mantener intacto el código fuente de cada módulo, colocándolo en un arnés de prueba con dependencias externas burladas.Mi sugerencia aquí sería dar el paso aún más valiente de intentar algunas refactorizaciones para eliminar (o invert) las dependencias, que es probablemente el mismo paso que estás tratando de evitar.

Sugiero esto porque supongo que las dependencias te van a matar a medida que escribes las pruebas. Seguramente estará mejor a largo plazo si puede eliminar las dependencias.

1

No sé si esto funcionará para su proyecto, pero puede tratar de atacar el problema desde el fase de enlace de su compilación.

Esto eliminaría por completo su problema de #include. Todo lo que tendría que hacer es volver a implementar las interfaces en los archivos incluidos para hacer lo que quiera y luego simplemente vincular a los archivos de objetos simulados que ha creado para implementar las interfaces en el archivo de inclusión.

La gran desventaja de este método es un sistema de compilación más compilado.