2009-06-29 17 views
5

¿Está bien romper todas las dependencias usando interfaces solo para hacer que una clase sea comprobable? Implica una sobrecarga significativa en el tiempo de ejecución debido a muchas llamadas virtuales en lugar de invocaciones de métodos simples.pruebas de unidad en C++

¿Cómo funciona el desarrollo impulsado por pruebas en aplicaciones de C++ del mundo real? Leí Working Effectively With Legacy Code y me gusta mucho pero no me esfuerzo por practicar TDD.

Si realizo una refactorización, ocurre muy a menudo que tengo que volver a escribir completamente la prueba unitaria debido a los cambios lógicos masivos. Mi código cambia muy a menudo y cambia la lógica fundamental del procesamiento de datos. No veo una manera de escribir pruebas unitarias que no tengan que cambiar en una refactorización grande.

Puede haber alguien que me indique una aplicación C++ de código abierto que utiliza TDD para aprender con el ejemplo.

Respuesta

5

actualización: Ver this question too.

sólo puedo responder a algunas partes aquí:

está bien para romper todas las dependencias que utilizan las interfaces sólo para hacer una clase comprobable? Implica una sobrecarga significativa en el tiempo de ejecución debido a muchas llamadas virtuales en lugar de invocaciones de métodos simples.

Si su rendimiento sufrirá demasiado debido a ello, no (punto de referencia!). Si su desarrollo sufre demasiado, no (estimar el esfuerzo extra). Si parece que no va a importar mucho y ayuda a la larga y te ayuda con la calidad, sí.

Siempre podría 'amigo' sus clases de prueba, o un objeto TestAccessor a través del cual sus pruebas podrían investigar cosas dentro de él. Eso evita hacer todo dinámicamente despachable solo para probar. (suena como un buen trabajo).

El diseño de interfaces comprobables no es fácil. A veces debes agregar algunos métodos adicionales que acceden a las entrañas solo para probar. Te hace temblar un poco, pero es bueno tenerlo y la mayoría de las veces esas funciones también son útiles en la aplicación real, tarde o temprano.

Si realizo una refactorización, ocurre con mucha frecuencia que tengo que volver a escribir completamente la prueba unitaria debido a cambios masivos en la lógica. Mi código cambia muy a menudo y cambia la lógica fundamental del procesamiento de datos. No veo una manera de escribir pruebas unitarias que no tengan que cambiar en una refactorización grande.

Las grandes refactorizaciones por definición cambian mucho, incluidas las pruebas. Se feliz de tenerlos, ya que probarán cosas después de refaccionar también.

Si dedica más tiempo a refactorizar que a crear nuevas funciones, tal vez debería pensar un poco más antes de codificar para encontrar mejores interfaces que puedan soportar más cambios. Además, escribir pruebas unitarias antes de que las interfaces sean estables es un problema, no importa lo que hagas.

Cuantos más códigos tenga contra una interfaz que cambia mucho, más código tendrá que cambiar cada vez. Creo que tu problema yace allí. Logré tener interfaces suficientemente estables en la mayoría de los lugares, y refactorizar solo partes de vez en cuando.

Espero que ayude.

+0

No es que no piense antes de codificar. Muy a menudo los requisitos cambian después de que el cliente haya visto la implementación inicial de una nueva característica. Otra razón es que a veces es necesario realizar una implementación rápida y sucia por motivos de marketing. En este caso, no me molesto con las pruebas unitarias, pero a veces el truco rápido y sucio se convierte en algo real debido a una estrecha programación. No es fácil hacer que este desastre sea comprobable más adelante. – frast

+0

Lo siento, no quise dar a entender exactamente eso. ;) Sé que la vida real es difícil de trabajar a veces. Quick'n dirty hacks viene con una deuda, ya sea prueba faltante, refactorización/limpieza necesaria o ambos. O bien paga en efectivo primero (trabajo adecuado) o paga con el alquiler después (limpieza). Sin moverse de eso. Es difícil trabajar en condiciones en las que no se puede influir en eso. (Estado allí ...) – Macke

3

He utilizado rutinariamente macros, #if y otros trucos de preprocesador para "fallar" las dependencias con el fin de probar la unidad en C y C++, exactamente porque con tales macros no tengo que pagar ningún tiempo de ejecución costo cuando el código se compila para producción en lugar de prueba. No es elegante, pero es razonablemente efectivo.

En cuanto a refactorizaciones, bien pueden requerir cambiar las pruebas cuando son tan abrumadoramente grandes e intrusivas como usted describe. Sin embargo, no me encuentro refaccionando tan drásticamente con tanta frecuencia.

+1

Pienso en su solución, pero me temo que podría ocurrir que no esté probando el "código real" porque el preprocesador lo define. Pero creo que vale la pena intentarlo. – frast

+0

Sí, las macros y otros hacks de preprocesador son cosas frágiles a menos que se utilicen con cuidado y de acuerdo con patrones específicos y limitados: esa es la parte "no elegante" en mi respuesta.El macroequivalente de inyección de dependencia (a veces también se puede lograr con plantillas o typedefs, como dice @jaif, en casos simples) es un caso bastante limitado y disciplinado, y eso es todo lo que necesitas para que puedas "burlarse" de tus dependencias para fines de pruebas unitarias –

1

La respuesta obvia sería factorizar dependencias utilizando plantillas en lugar de interfaces. Por supuesto, eso podría perjudicar los tiempos de compilación (dependiendo de cómo lo implemente exactamente), pero debería eliminar al menos la sobrecarga del tiempo de ejecución. Una solución ligeramente más simple podría ser simplemente confiar en un conjunto de tipos de archivos, que pueden intercambiarse con algunas macros o similares.

1

En cuanto a su primera pregunta, rara vez vale la pena romper las cosas por el simple hecho de probar, aunque a veces es posible que tenga que romper las cosas antes de mejorarlas como parte de su refactorización. El criterio más importante para un producto de software es que funciona, no que sea comprobable. El valor comprobable solo es importante, ya que lo ayuda a crear un producto que sea más estable y funcione mejor para sus usuarios finales.

Una gran parte del desarrollo basado en pruebas es seleccionar pequeñas partes atómicas de su código que no es probable que cambien para la prueba unitaria. Si tiene que volver a escribir muchas pruebas unitarias debido a cambios lógicos masivos, es posible que deba probar en un nivel más detallado o rediseñar el código para que sea más estable. Un diseño estable no debería cambiar drásticamente con el tiempo, y las pruebas no lo ayudarán a evitar una refacturación masiva si se requiere. Sin embargo, si se realizan las pruebas correctas, es posible que, cuando refaccione las cosas, pueda estar más seguro de que su refactorización fue exitosa, suponiendo que haya algunas pruebas que no deban modificarse.

1

¿Está bien romper todas las dependencias usando interfaces solo para hacer que una clase sea comprobable? Implica una sobrecarga significativa en el tiempo de ejecución debido a muchas llamadas virtuales en lugar de invocaciones de métodos simples.

Creo que está bien romper las dependencias, ya que eso conducirá a mejores interfaces.

Si realizo una refactorización, ocurre con mucha frecuencia que tengo que volver a escribir completamente la prueba unitaria debido a cambios masivos en la lógica. Mi código cambia muy a menudo y cambia la lógica fundamental del procesamiento de datos. No veo una manera de escribir pruebas unitarias que no tengan que cambiar en una refactorización grande.

No recibirá estas grandes refactorizaciones en ningún idioma ya que sus pruebas deberían expresar la intención real de su código. Entonces, si la lógica cambia, tus pruebas deben cambiar.

Tal vez usted no está realmente haciendo TDD, como:

  1. Crear una prueba que falla
  2. crear el código para pasar la prueba
  3. Crear otra prueba que falle
  4. Fijar el código para pasar ambas pruebas
  5. Enjuague y repita hasta que crea que tiene suficientes pruebas que muestran lo que su código debería estar haciendo

Estos pasos dicen que debe hacer cambios menores, y no grandes. Si te quedas con este último, no puedes escapar de grandes refactores.Ningún idioma lo salvará de eso, y C++ será el peor de ellos debido a los tiempos de compilación, tiempos de enlace, mensajes de error incorrectos, etc. etc.

Estoy trabajando en un software del mundo real escrito en C++ con un enorme código heredado debajo. Estamos usando TDD y realmente está ayudando a evolucionar el diseño del software.

0

Si hago una refactorización, ocurre muy a menudo que tengo que reescribir completamente la prueba unitaria debido a cambios masivos en la lógica. ... No veo una manera de escribir pruebas unitarias que no tengan que cambiar en una refactorización grande.

There are multiple layers of testing, y algunas de esas capas no se romperán incluso después de grandes cambios en la lógica. La prueba unitaria, por otro lado, está destinada a probar las partes internas de los métodos y objetos, y tendrá que cambiar más a menudo que eso. No hay nada malo, necesariamente. Así son las cosas.

¿Está bien [...] romper todas las dependencias usando interfaces solo para hacer que una clase sea comprobable?

It's definitely OK to design classes to be more testable. Eso es parte del propósito de TDD, después de todo.

Implica una sobrecarga significativa en el tiempo de ejecución debido a muchas llamadas virtuales en lugar de invocaciones de método simple.

Casi todas las empresas tienen una lista de reglas que todos los empleados deben seguir. Las empresas desorientadas simplemente enumeran todas las buenas calidades en las que pueden pensar ("nuestros empleados son eficientes, responsables, éticos y nunca cortan las esquinas"). Las empresas más inteligentes realmente clasifican sus prioridades. Si alguien presenta una forma poco ética de ser eficiente, ¿lo hace la empresa? Las mejores compañías no solo imprimen folletos que dicen cómo se clasifican las prioridades, sino que también se aseguran de que la administración siga el ranking.

Es muy posible que un programa sea eficiente y fácil de probar. Sin embargo, hay momentos en los que debe elegir cuál es más importante. Éste es uno de esos momentos. No sé qué tan importante es la eficiencia para usted y su programa, pero lo hace. Entonces, "¿preferiría tener un programa lento y bien probado, o un programa rápido sin una cobertura de prueba total?"

+0

Usted hace muy buenos puntos. A su última pregunta, creo que deberíamos pensar en la cobertura de rendimiento frente a prueba de un caso a otro. No todas las partes de nuestra aplicación necesitan un rendimiento máximo. – frast

0

Se trata de una sobrecarga significativa en el tiempo de ejecución debido a muchas llamadas virtuales en vez de invocaciones de métodos simples.

Recuerde que solo se trata de una sobrecarga de llamada virtual si accede al método mediante un puntero (o ref.) A su interfaz u objeto. Si accede al método a través de un objeto concreto en la pila, no tendrá la sobrecarga virtual, e incluso puede estar en línea.

Además, nunca asuma que esta sobrecarga es grande antes de perfilar su código. Casi siempre, una llamada virtual no tiene valor si se compara con lo que está haciendo su método. (La mayor parte de la penalización proviene de la imposibilidad de alinear un método de una línea, no del indirecto adicional de la llamada).