refactoring - refactory - ¿Cómo facilita TDD la refactorización?
refactorizar sinonimo (8)
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.
Encuentro 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, las nuevas clases se agregan y eliminan, 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 de la unidad podría ser de gran ayuda durante el mantenimiento, una vez que el software esté construido, estable y funcione, 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?
¿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.
El objetivo de las pruebas y especificaciones es definir el comportamiento correcto de un sistema. Entonces, muy simple:
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
Entonces, si las especificaciones de la aplicación / sistema o el comportamiento deseado cambian, es necesario 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. :)
cambiando un algoritmo por uno más eficiente, por ejemplo.
Esto no es refactorización, esta es la optimización del rendimiento . Refactorizar consiste en mejorar 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 en que produce ese resultado.
El libro TDD de Kent Beck.
Prueba primero Seguir los principios de SOLO OOP y utilizar una buena herramienta de refactorización son indispensables, si no es necesario.
Los principales beneficios 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.
Recomendaría (como otros lo han hecho):
- Refactorización: mejorar el diseño del código existente - Martin Fowler
- Desarrollo impulsado por prueba: por ejemplo - Kent Beck
- Patrones de arquitectura empresarial - Fowler et al.
TDD dice escribir primero una prueba de falla . 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, escribe el código para cumplir con la prueba.
Si el requisito cambia o ha sido mal entendido, edite o reescriba primero la prueba.
Red-bar, Green-bar, ¿verdad?
Refactoring de Fowler es la referencia para la refactorización, por extraño que parezca.
La serie de artículos de Scott Ambler en Dr. Dobb''s (''The Agile Edge ??'') es un gran tutorial de TDD en la práctica.
Una cosa que debes tener en cuenta es que TDD no es principalmente una estrategia de prueba, sino 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 primero cambias las pruebas, 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 ocurrir 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 volverse 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 todavía, pero he escuchado cosas buenas sobre el libro "Patrones de prueba XUnit - Código de prueba de refactorización".
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 de la unidad podría ser de gran ayuda durante el mantenimiento, una vez que el software esté construido, estable y funcione, pero ya es tarde cuando se supone que TDD también lo ayudará desde el principio.
Estoy de acuerdo en que la sobrecarga de tener un conjunto de pruebas unitarias puede sentirse en estos cambios tempranos, cuando se están produciendo cambios arquitectónicos importantes, pero mi opinión es que los beneficios de tener pruebas unitarias superan con creces este inconveniente. 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 se están llevando a cabo verdaderamente solo en refactorizaciones? Si solo está refactorizando, aunque sea de manera espectacular, y las pruebas comiencen a fallar, eso puede indicarle que ha cambiado la funcionalidad de alguna otra manera inadvertida. 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.
Actualización (basada en comentarios):
@ Cybis ha planteado una interesante objeción a mi afirmación de que la refacturación no debería romper las pruebas porque la refacturación no debería cambiar el comportamiento. Su objeción es que la refactorización cambia la API, y por lo tanto las pruebas "se rompen".
Primero, alentaría a cualquiera a visitar la referencia canónica sobre la refactorización: Bliki de Martin Fowler . Justo ahora lo he revisado y un par de cosas saltan a la vista:
- ¿Cambiar una interfaz de refactorización? Martin se refiere a la refactorización como un cambio de "conservación del comportamiento", lo que significa que cuando la interfaz / API cambia, todas las personas que llaman de esa interfaz / API deben cambiar también. Incluyendo pruebas, digo.
- Eso no quiere decir que el comportamiento haya cambiado. Nuevamente, Fowler enfatiza que su definición de refactorización es que los cambios son preservativos del comportamiento.
A la luz de esto, si una prueba o pruebas tienen que cambiar durante una refactorización, no veo esto como una "ruptura" de 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 sobre considerar las pruebas como ciudadanos de primera clase del código base).
Además, esperaría que las pruebas, incluso las pruebas modificadas, continúen pasando una vez que se haya completado la refactorización. 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 una tontería, 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 funcionando 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 el reemplazo de una Lista Vinculada con una Lista de Arreglos o algo así. Pero en ese caso podría argumentarse que las pruebas están exagerando y son demasiado rígidos y frágiles).