En otra pregunta, se reveló que uno de los dolores con TDD es mantener el paquete de pruebas sincronizado con la base de código durante y después de la refactorización.
Ahora, soy un gran admirador de la refactorización. No voy a renunciar a hacer TDD. Pero también he experimentado los problemas de las pruebas escritas de tal manera que una refactorización menor conduce a muchas fallas en las pruebas.
¿Cómo evitas romper las pruebas al refactorizar?
- ¿Escribes las pruebas 'mejor'? Si es así, ¿qué debe buscar?
- ¿Evitas ciertos tipos de refactorización?
- ¿Existen herramientas de refactorización de pruebas?
Editar: escribí una nueva pregunta que preguntaba qué quería hacer (pero mantuve esta como una variante interesante).
development-process
tdd
refactoring
Alex Feinman
fuente
fuente
Respuestas:
Lo que intentas hacer no es refactorizar realmente. Con la refactorización, por definición, no cambia lo que hace su software, cambia cómo lo hace.
Comience con todas las pruebas ecológicas (todas pasan), luego realice modificaciones "debajo del capó" (por ejemplo, mueva un método de una clase derivada a la base, extraiga un método o encapsule un Compuesto con un Constructor , etc.). Tus pruebas aún deberían pasar.
Lo que estás describiendo no parece ser una refactorización, sino un rediseño, que también aumenta la funcionalidad de tu software bajo prueba. TDD y refactorización (como intenté definirlo aquí) no están en conflicto. Todavía puede refactorizar (verde-verde) y aplicar TDD (rojo-verde) para desarrollar la funcionalidad "delta".
fuente
Uno de los beneficios de tener pruebas unitarias es que puede refactorizar con confianza.
Si la refactorización no cambia la interfaz pública, entonces deja las pruebas unitarias como están y se asegura que después de refactorizar todas pasen.
Si la refactorización cambia la interfaz pública, entonces las pruebas deben reescribirse primero. Refactorizar hasta que pasen las nuevas pruebas.
Nunca evitaría ninguna refactorización porque rompe las pruebas. Escribir pruebas unitarias puede ser un dolor en el trasero, pero vale la pena a largo plazo.
fuente
Al contrario de las otras respuestas, es importante tener en cuenta que algunas formas de prueba pueden volverse frágiles cuando se refactoriza el sistema bajo prueba (SUT), si la prueba es una caja blanca.
Si estoy usando un marco burlón que verifica el orden de los métodos invocados en los simulacros (cuando el orden es irrelevante porque las llamadas están libres de efectos secundarios); entonces, si mi código está más limpio con esas llamadas a métodos en un orden diferente y refactorizo, entonces mi prueba se interrumpirá. En general, los simulacros pueden introducir fragilidad en las pruebas.
Si estoy verificando el estado interno de mi SUT al exponer a sus miembros privados o protegidos (podríamos usar "amigo" en Visual Basic, o escalar el nivel de acceso "interno" y usar "internalsvisibleto" en c #; en muchos idiomas OO, incluidos c # se podría usar una " subclase específica de prueba "), de repente, el estado interno de la clase será importante: puede que esté refactorizando la clase como un cuadro negro, pero las pruebas de cuadro blanco fallarán. Suponga que un solo campo se reutiliza para significar cosas diferentes (¡no es una buena práctica!) Cuando el SUT cambia de estado; si lo dividimos en dos campos, es posible que necesitemos reescribir pruebas rotas.
Las subclases específicas de prueba también se pueden usar para probar métodos protegidos, lo que puede significar que un refactorizador desde el punto de vista del código de producción es un cambio importante desde el punto de vista del código de prueba. Mover algunas líneas dentro o fuera de un método protegido puede no tener efectos secundarios de producción, pero interrumpa una prueba.
Si uso " ganchos de prueba " o cualquier otro código de compilación condicional o específico de prueba, puede ser difícil garantizar que las pruebas no se rompan debido a las dependencias frágiles de la lógica interna.
Por lo tanto, para evitar que las pruebas se acoplen a los detalles internos íntimos del SUT, puede ayudar a:
Todos los puntos anteriores son ejemplos de acoplamiento de caja blanca utilizados en las pruebas. Por lo tanto, para evitar por completo la refactorización de las pruebas de ruptura, utilice las pruebas de caja negra del SUT.
Descargo de responsabilidad: con el propósito de discutir la refactorización aquí, estoy usando la palabra un poco más ampliamente para incluir cambios en la implementación interna sin ningún efecto externo visible. Algunos puristas pueden estar en desacuerdo y referirse exclusivamente al libro Refactoring de Martin Fowler y Kent Beck, que describe las operaciones de refactorización atómica.
En la práctica, tendemos a dar pasos que no se rompen un poco más grandes que las operaciones atómicas descritas allí, y en particular los cambios que hacen que el código de producción se comporte de manera idéntica desde el exterior pueden no dejar pasar las pruebas. Pero creo que es justo incluir "algoritmo sustituto de otro algoritmo que tenga un comportamiento idéntico" como refactorizador, y creo que Fowler está de acuerdo. El propio Martin Fowler dice que la refactorización puede romper las pruebas:
fuente
Si sus pruebas se rompen cuando está refactorizando, entonces no está, por definición, refactorizando, lo que significa "cambiar la estructura de su programa sin cambiar el comportamiento de su programa".
A veces, usted necesita cambiar el comportamiento de sus pruebas. Tal vez necesite fusionar dos métodos (por ejemplo, bind () y listen () en una clase de socket TCP de escucha), por lo que tiene otras partes de su código que intentan y no pueden usar la API ahora alterada. ¡Pero eso no es refactorizar!
fuente
Creo que el problema con esta pregunta es que diferentes personas están tomando la palabra 'refactorizar' de manera diferente. Creo que es mejor definir cuidadosamente algunas cosas que probablemente quieras decir:
Como ya señaló otra persona, si mantiene la API igual y todas sus pruebas de regresión operan en la API pública, no debería tener problemas. La refactorización no debería causar ningún problema. Cualquier prueba fallida CUALQUIERA significa que su código anterior tenía un error y su prueba es mala, o su nuevo código tiene un error.
Pero eso es bastante obvio. Entonces, PROBABLEMENTE quieres decir con refactorización, que estás cambiando la API.
¡Déjenme responder cómo abordar eso!
Primero cree una NUEVA API, que haga lo que desea que sea su NUEVO comportamiento de API. Si sucede que esta nueva API tiene el mismo nombre que una API ANTERIOR, entonces agrego el nombre _NUEVO al nuevo nombre de la API.
int DoSomethingInterestingAPI ();
se convierte en:
OK, en esta etapa, todas sus pruebas de regresión se aprobarán utilizando el nombre DoSomethingInterestingAPI ().
SIGUIENTE, revise su código y cambie todas las llamadas a DoSomethingInterestingAPI () a la variante apropiada de DoSomethingInterestingAPI_NEW (). Esto incluye actualizar / reescribir las partes de las pruebas de regresión que se deban cambiar para usar la nueva API.
SIGUIENTE, marque DoSomethingInterestingAPI_OLD () como [[en desuso ()]]. Mantenga la API obsoleta todo el tiempo que desee (hasta que haya actualizado de forma segura todo el código que pueda depender de ella).
Con este enfoque, cualquier falla en sus pruebas de regresión simplemente son errores en esa prueba de regresión o identifican errores en su código, exactamente como lo desearía. Este proceso por etapas de revisión de una API mediante la creación explícita de versiones _NEW y _OLD de la API le permite tener partes del código nuevo y antiguo coexistiendo por un tiempo.
fuente
Supongo que sus pruebas unitarias son de una granularidad que yo llamaría "estúpido" :) es decir, prueban las minucias absolutas de cada clase y función. Aléjese de las herramientas generadoras de código y escriba pruebas que se apliquen a una superficie más grande, luego puede refactorizar las partes internas tanto como desee, sabiendo que las interfaces para sus aplicaciones no han cambiado y sus pruebas aún funcionan.
Si desea tener pruebas unitarias que prueben todos y cada uno de los métodos, espere tener que refactorizarlos al mismo tiempo.
fuente
Lo que lo hace difícil es el acoplamiento . Cualquier prueba viene con cierto grado de acoplamiento con los detalles de implementación, pero las pruebas unitarias (independientemente de si es TDD o no) son especialmente malas porque interfieren con las partes internas: más pruebas unitarias equivalen a más código acoplado a las unidades, es decir, firmas de métodos / cualquier otra interfaz pública de unidades, al menos.
Las "unidades", por definición, son detalles de implementación de bajo nivel, la interfaz de las unidades puede y debe cambiar / dividirse / fusionarse y, de lo contrario, mutar a medida que el sistema evoluciona. La abundancia de pruebas unitarias puede obstaculizar esta evolución más de lo que ayuda.
¿Cómo evitar romper las pruebas al refactorizar? Evitar el acoplamiento. En la práctica, significa evitar tantas pruebas unitarias como sea posible y preferir pruebas de mayor nivel / integración más agnósticas de los detalles de implementación. Sin embargo, recuerde que no hay una viñeta plateada, las pruebas aún tienen que acoplarse a algo en algún nivel, pero idealmente debería ser una interfaz que se versione explícitamente usando el Versionado Semántico, es decir, generalmente en el nivel de aplicación / aplicación publicada (no desea hacer SemVer para cada unidad en su solución).
fuente
Sus pruebas están demasiado unidas a la implementación y no al requisito.
considere escribir sus pruebas con comentarios como este:
de esta manera no puede refactorizar el significado de las pruebas.
fuente