2008-11-01 10 views
25

He oído que los proyectos desarrollados con TDD son más fáciles de refactorizar porque la práctica arroja un conjunto completo de pruebas unitarias, que (afortunadamente) fallarán si algún cambio ha roto el código. Todos los ejemplos que he visto de esto, sin embargo, tratan de la refactorización de la implementación, cambiando un algoritmo por uno más eficiente, por ejemplo.¿Cómo facilita TDD la refactorización?

Me parece que la arquitectura de refactorización es mucho más común en las primeras etapas donde el diseño aún se está trabajando. Las interfaces cambian, se agregan nuevas clases & eliminado, incluso el comportamiento de una función podría cambiar ligeramente (pensé que lo necesitaba para hacerlo, pero en realidad tiene que hacer eso), etc. Pero si cada caso de prueba está estrechamente acoplado a estas clases inestables, ¿no tendría que estar constantemente reescribiendo sus casos de prueba cada vez que cambie un diseño?

¿Bajo qué situaciones en TDD está bien alterar y eliminar casos de prueba? ¿Cómo puede estar seguro de que alterar los casos de prueba no los rompe? Además, parece que tener que sincronizar un conjunto de pruebas completo con un código constantemente cambiante sería una molestia. Entiendo que el conjunto de pruebas unitarias podría ser de gran ayuda durante el mantenimiento, una vez que el software esté construido, estable y en funcionamiento, pero ya es tarde cuando se supone que TDD también lo ayudará desde el principio.

Por último, ¿un buen libro sobre TDD y/o refactorización abordaría este tipo de problemas? Si es así, ¿cuál recomendarías?

+0

He estado pensando en lo mismo. En cierto modo, podría decirse que las pruebas violan DRY, ya que el comportamiento de un fragmento de código se refleja tanto en ese código como en el código que lo prueba. – Boris

Respuesta

8

Además de que parece que el tener que sincronizar una suite completa de los ensayos con código en constante cambio sería un dolor. Entiendo que la unidad de prueba de la unidad podría ayudar tremendamente a durante el mantenimiento, una vez que el software esté construido, estable y en funcionamiento, pero que está retrasado en el juego cuando TDD es supuestamente también desde el principio.

Estoy de acuerdo en que la sobrecarga de tener un conjunto de pruebas de unidad en su lugar se puede sentir en estos primeros cambios, cuando los principales cambios arquitectónicos están llevando a cabo, pero mi opinión es que los beneficios de tener pruebas unitarias son muy superiores a este retirarse. Con frecuencia pienso que el problema es mental: tendemos a pensar en nuestras pruebas unitarias como ciudadanos de segunda clase del código base, y nos molesta tener que meternos con ellos. Pero con el tiempo, a medida que he llegado a depender de ellos y aprecio su utilidad, he llegado a pensar en ellos como no menos importantes y no menos dignos de mantenimiento y trabajo que cualquier otra parte de la base de códigos.

¿Los principales "cambios" arquitectónicos tienen lugar realmente solo en refactorizaciones? Si solo está refacturando, aunque sea de manera espectacular, y las pruebas comiencen a fallar, eso puede indicarle que ha cambiado la funcionalidad de alguna otra manera. Que es exactamente lo que se supone que las pruebas unitarias te ayudarán a atrapar. Si realiza cambios radicales en la funcionalidad y la arquitectura al mismo tiempo, le recomendamos que reduzca la velocidad y entre en ese surco rojo/verde/refactor: sin funcionalidad nueva (o modificada) sin pruebas adicionales, y sin cambios en funcionalidad (y pruebas de interrupción) durante la refactorización.

Update (basado en los comentarios):

@Cybis ha planteado una objeción interesante para mi afirmación de que la refactorización no debe romper pruebas porque refactorización no debe cambiar el comportamiento. Su objeción es que refactorizar hace cambiar la API, y por lo tanto prueba "break".

En primer lugar, recomendaría a cualquiera que visite la referencia canónica sobre refactorización: Martin Fowler's bliki. Justo ahora he revisado y un par de cosas saltan a mí:

  • Is changing an interface refactoring? Martin se refiere a refactorización como un cambio "comportamiento de preservación" , que significa cuando la interfaz/API cambia entonces todas las personas que llaman de esa interfaz /API debe cambiar también. Incluyendo pruebas, digo.
  • Eso no significa que el comportamiento ha cambiado. Nuevamente, Fowler enfatiza que su definición de refactorización es que los cambios son del comportamiento preservando.

En vista de esto, si una prueba o pruebas tienen que cambiar durante una refactorización, no creo que esto "rompa" la (s) prueba (s). Es simplemente parte de la refactorización, de preservar el comportamiento de toda la base de códigos.No veo diferencia entre que una prueba tenga que cambiar y que cualquier otra parte de la base de código tenga que cambiar como parte de una refactorización. (Esto se remonta a lo que dije antes de considerar las pruebas para ser ciudadanos de primera clase de la base de código.)

Además, yo esperaría que las pruebas, incluso las pruebas modificadas, a continuar pasando una vez que la refactorización está hecho. Lo que sea que esa prueba estaba probando (probablemente la afirmación (es) en esa prueba) aún debería ser válida después de que se realiza una refactorización. De lo contrario, es una señal de alerta que el comportamiento cambió/retrocedió de alguna manera durante la refactorización. Quizás esa afirmación parezca absurda, pero piénselo: no creemos en mover bloques de código en la base del código de producción y esperar que continúen trabajando en su nuevo contexto (nueva clase, nueva firma de método, lo que sea) . Siento lo mismo con una prueba: quizás una refactorización cambia la API que debe llamar una prueba, o una clase que una prueba debe usar, pero al final el punto de la prueba no debería cambiar debido a una refactorización.

(La única excepción que puedo pensar en esto son las pruebas que prueban los detalles de implementación de bajo nivel que puede querer cambiar durante una refactorización, como reemplazar una lista vinculada por una lista de arreglos o algo así. Pero en ese caso uno podría argumentan que las pruebas son excesivas y son demasiado rígidas y frágiles.)

+1

Nunca entendí por qué la gente dice "la refacturación no debería romper las pruebas porque el comportamiento no cambia". ¡Las interfaces cambian! Si tiene una función grande y un par de docenas de pruebas unitarias para ella, probando diferentes condiciones de contorno, ¡todas se rompen tan pronto como usted refactoriza la función en otras más pequeñas! – Cybis

+1

¿Por qué no puedo editar comentarios? Arg. Lo que quise decir es que las pruebas se rompen si refactoriza las interfaces de manera que necesita llamar a diferentes funciones para acceder al mismo comportamiento (este tipo de refactorización puede mejorar la legibilidad y no debe evitarse). – Cybis

+0

Gracias por los comentarios @Cybis, he actualizado mi respuesta en respuesta a ellos. –

4

TDD dice escribir falla primero prueba. La prueba está escrita para mostrar que el desarrollador entiende lo que se supone que debe lograr el caso de uso/historia/escenario/proceso.

Luego, escriba el código para cumplir con la prueba.

Si el requisito cambia o ha sido mal interpretado, edite o reescriba primero la prueba.

Barra roja, barra verde, ¿verdad?

de Fowler Refactoring es la referencia de refactorización, por extraño que parezca. La serie de

Scott Ambler de artículos endel Dr. Dobb ('El ágil Edge ??') es un gran tutorial de TDD en la práctica.

6

Las principales ventajas de TDD para refactorizar es que el desarrollador tiene más valor para cambiar su código. Con la prueba de unidad lista, los desarrolladores se atreven a cambiar el código y luego simplemente ejecutarlo. Si la barra xUnit todavía está verde, tienen confianza para seguir adelante.

Personalmente, me gusta TDD, pero no fomenta over-TDD. Es decir, no escriba demasiados casos de prueba unitaria. Las pruebas unitarias deberían ser suficientes. Si superas las pruebas unitarias, es posible que te encuentres en un dilema cuando quieras realizar un cambio de arquitectura. Un gran cambio en el código de producción hará que cambien muchos casos de pruebas unitarias. Por lo tanto, solo mantenga su unidad de prueba lo suficiente.

+1

El argumento, sin embargo, es que TDD parcial no es TDD. Escribir código directamente sin pruebas primero rompe el mantra completo de TDD. ¿Cómo es posible no sobre-tdd si todo el código se escribe primero en prueba? – Cybis

1

Kent Beck's TDD book.

Pruebe primero. Seguir los principios de S.O.L.I.D OOP y utilizar una buena herramienta de refactorización son indispensables, si no es necesario.

+0

¿Qué es S.O.L.I.D? por favor explique. – Tilendor

+0

SOLID es principios de diseño de Robert Martins de 'Desarrollo de software ágil, principios, patrones y prácticas': Principio de responsabilidad única, Principio abierto cerrado, Principio de sustitución de Liscov, Principio de segregación de interfaz e Principio de inversión de dependencia. – quamrana

1

¿Bajo qué situaciones en TDD está bien alterar y eliminar los casos de prueba? ¿Cómo puede estar seguro de que alterar los casos de prueba no los rompe? Además, parece que tener que sincronizar un conjunto de pruebas completo con un código constantemente cambiante sería una molestia.

El objetivo de las pruebas y especificaciones es definir el comportamiento correcto de un sistema. Por lo tanto, muy simplemente:

if definition of correctness changes 
    change tests/specs 
end 

if definition of correctness does not change 
    # no need to change tests/specs 
    # though you still can for other reasons if you want/need 
end 

lo tanto, si las especificaciones de la aplicación/sistema o cambios de comportamiento deseados, es una necesidadpara cambiar las pruebas. Cambiar solo el código, pero no las pruebas, en tal situación es obviamente una metodología rota. Puede verlo como "un dolor", pero no tener un conjunto de pruebas es más doloroso. :) Como otros lo han mencionado, tener esa libertad para "atreverse" a cambiar el código es muy empoderador y liberador. :)

3

cambiando un algoritmo por uno más eficiente, por ejemplo.

Esto no es refactorización, esta es la optimización del rendimiento . Refactorizar es aproximadamente mejorando el diseño del código existente. Es decir, cambiando su forma para satisfacer mejor las necesidades del desarrollador. Cambiar el código con la intención de afectar el comportamiento visible externamente no es una refactorización, y eso incluye cambios por eficiencia.

Parte del valor de TDD es que sus pruebas lo ayudan a mantener el comportamiento visible constante mientras cambia la forma de producir ese resultado.

+0

Lo sé. Los algoritmos de conmutación son un problema de optimización, no de refactoración. Refactorizar es reorganizar el código - el comportamiento de movimiento a clases más apropiadas, dividiendo clases grandes en otras más pequeñas y más cohesivas, etc. Simplemente, muchos artículos en línea no muestran ejemplos realistas de esto. – Cybis

10

Una cosa que debe tener en cuenta es que TDD es no principalmente una estrategia de prueba, pero una estrategia de diseño. Primero escribe las pruebas, porque eso le ayuda a encontrar un mejor diseño desacoplado. Y un mejor diseño desacoplado también es más fácil de refactorizar.

Cuando cambia la funcionalidad de una clase o método, es natural que las pruebas también cambien. De hecho, seguir TDD significaría que usted cambia las pruebas primero, por supuesto. Si tiene que cambiar muchas pruebas para cambiar solo un poco de funcionalidad, eso normalmente significa que la mayoría de las pruebas están sobreespecificando el comportamiento; están probando más de lo que deberían probar. Otro problema podría ser que una responsabilidad no está bien encapsulada en su código de producción.

Sea lo que sea, cuando experimenta muchas pruebas que fallan debido a un pequeño cambio, debe refactorizar su código para que no vuelva a suceder en el futuro. Siempre es posible hacer eso, aunque no siempre es obvio cómo hacerlo.

Con cambios de diseño más grandes, las cosas pueden ser un poco más complicadas. Sí, a veces será más fácil escribir nuevas pruebas y descartar las antiguas.A veces, al menos puede escribir algunas pruebas de integración que prueban toda la parte que se refactoriza. Y es de esperar que todavía tenga su conjunto de pruebas de aceptación, que en su mayoría no se ven afectadas.

No lo he leído aún, pero he escuchado cosas buenas sobre el libro "Patrones de prueba XUnit - Código de prueba de refactorización".

+0

¿Qué haces en los casos en que un solo algoritmo tiene muchas, muchas condiciones de contorno? El análisis de texto con una gramática BNF no trivial es un buen ejemplo. Tienes un analizador a medio terminar, pero luego necesitas cambiar la gramática ligeramente. Debido a que su árbol de sintaxis cambia, todas las pruebas se rompen. Damn 300 char lim – Cybis

+0

Todas las pruebas no * necesitan * para romperse. Necesita cambiar su diseño para que siga el Principio de Elección Única - debe haber exactamente un lugar en su analizador que se vea afectado por este pequeño cambio - y solo las pruebas para esta parte del analizador deberían modificarse. –

+0

Cuando habla de una estrategia de diseño, ¿cómo piensa en ** escribir ** su código como si fuera probado, pero no necesariamente escribir las pruebas desde el principio? –