Estoy refactorizando una gran clase de código heredado. La refactorización (supongo) aboga por esto:
- escribir pruebas para la clase heredada
- refactorizar al diablo de la clase
Problema: una vez que refactorice la clase, mis pruebas en el paso 1 deberán cambiarse. Por ejemplo, lo que una vez estuvo en un método heredado, ahora puede ser una clase separada. Lo que era un método puede ser ahora varios métodos. Todo el paisaje de la clase heredada puede ser borrado en algo nuevo, por lo que las pruebas que escribo en el paso 1 serán casi nulas y sin efecto. En esencia, agregaré el Paso 3. reescriba mis pruebas profusamente
¿Cuál es el propósito de escribir pruebas antes de refactorizar? Suena más como un ejercicio académico de crear más trabajo para mí. Estoy escribiendo pruebas para el método ahora y estoy aprendiendo más sobre cómo probar cosas y cómo funciona el método heredado. Uno puede aprender esto simplemente leyendo el código heredado, pero escribir pruebas es casi como frotarme la nariz y también documentar este conocimiento temporal en pruebas separadas. De esta manera, casi no tengo más remedio que aprender qué está haciendo el código. Dije temporal aquí, porque refactorizaré el código y toda mi documentación y pruebas serán nulas e inválidas durante una parte significativa, excepto que mi conocimiento se mantendrá y me permitirá estar más fresco en la refactorización.
¿Es esa la verdadera razón para escribir pruebas antes de refactorizar, para ayudarme a comprender mejor el código? Tiene que haber otra razón!
¡Por favor explique!
Nota:
Existe esta publicación: ¿Tiene sentido escribir pruebas para el código heredado cuando no hay tiempo para una refactorización completa? pero dice "escribir pruebas antes de refactorizar", pero no dice "por qué" o qué hacer si "escribir pruebas" parece "trabajo ocupado que se destruirá pronto"
fuente
Respuestas:
Refactorizar es limpiar un fragmento de código (por ejemplo, mejorar el estilo, el diseño o los algoritmos), sin cambiar el comportamiento (visible desde el exterior). Escribes pruebas para no asegurarte de que el código antes y después de la refactorización sea el mismo, sino que escribes pruebas como un indicador de que tu aplicación antes y después de la refactorización se comporta igual: el nuevo código es compatible y no se introdujeron nuevos errores.
Su principal preocupación debería ser escribir pruebas unitarias para la interfaz pública de su software. Esta interfaz no debería cambiar, por lo que las pruebas (que son una verificación automática para esta interfaz) tampoco deberían cambiar.
Sin embargo, las pruebas también son útiles para localizar errores, por lo que también puede tener sentido escribir pruebas para partes privadas de su software. Se espera que estas pruebas cambien a lo largo de la refactorización. Si desea cambiar un detalle de implementación (como el nombre de una función privada), primero actualice las pruebas para reflejar sus expectativas cambiadas, luego asegúrese de que la prueba falle (sus expectativas no se cumplen), luego cambie el código real y verifique que todas las pruebas pasen nuevamente. En ningún momento deberían comenzar a fallar las pruebas para la interfaz pública.
Esto es más difícil cuando se realizan cambios a mayor escala, por ejemplo, rediseñando múltiples partes codependientes. Pero habrá algún tipo de límite, y en ese límite podrás escribir pruebas.
fuente
Ah, manteniendo sistemas heredados.
Idealmente, sus pruebas tratan la clase solo a través de su interfaz con el resto de la base de código, otros sistemas y / o la interfaz de usuario. Interfaces No puede refactorizar la interfaz sin afectar esos componentes ascendentes o descendentes. Si todo es un desastre estrechamente acoplado, entonces también podría considerar el esfuerzo de reescribir en lugar de refactorizar, pero es en gran parte semántico.
Editar: Digamos que parte de su código mide algo y tiene una función que simplemente devuelve un valor. La única interfaz es llamar a la función / método / whatnot y recibir el valor devuelto. Este es un acoplamiento suelto y una prueba fácil de usar Si su programa principal tiene un subcomponente que administra un búfer, y todas las llamadas dependen del búfer en sí, algunas variables de control y devuelve los mensajes de error a través de otra sección de código, entonces podría decir que está estrechamente acoplado y es prueba difícil de unidad. Todavía puede hacerlo con cantidades suficientes de objetos simulados y demás, pero se vuelve desordenado. Especialmente en c. Cualquier cantidad de refactorización del funcionamiento del búfer romperá el subcomponente.
Fin de edición
Si está probando su clase a través de interfaces que permanecen estables, sus pruebas deben ser válidas antes y después de la refactorización. Esto le permite hacer cambios con la confianza de que no lo rompió. Al menos, más confianza.
También te permite hacer cambios incrementales. Si este es un gran proyecto, no creo que quiera desarmarlo todo, construir un nuevo sistema y luego comenzar a desarrollar pruebas. Puede cambiar una parte de ella, probarla y asegurarse de que el cambio no derribe el resto del sistema. O si lo hace, al menos puedes ver el desorden enredado gigante en desarrollo en lugar de sorprenderte cuando lo sueltes.
Si bien puede dividir un método en tres, seguirán haciendo lo mismo que el método anterior, por lo que puede tomar la prueba del método anterior y dividirlo en tres. El esfuerzo de escribir la primera prueba no se desperdicia.
Además, tratar el conocimiento del sistema heredado como "conocimiento temporal" no va a funcionar bien. Saber cómo lo hizo anteriormente es vital cuando se trata de sistemas heredados. Muy útil para la antigua pregunta de "¿por qué demonios hace eso?"
fuente
Mi propia respuesta / realización:
Después de corregir varios errores durante la refactorización, me estoy dando cuenta de que no habría realizado los movimientos del código tan fácilmente sin tener pruebas. Las pruebas me alertan de "diferencias" de comportamiento / funcionales que presento al cambiar mi código.
No tiene que estar hiperconsciente cuando tiene buenas pruebas en su lugar. Puede editar su código en un comportamiento más relajado. Las pruebas hacen la verificación y los controles de cordura por usted.
Además, mis pruebas se han mantenido más o menos igual que refactorizado y no fueron destruidas. De hecho, noté algunas oportunidades adicionales para agregar afirmaciones a mis pruebas a medida que profundizaba en el código.
ACTUALIZAR
Bueno, ahora estoy cambiando mucho mis pruebas: / Debido a que reescribí la función original (eliminé la función y creé una nueva clase de limpiador, moviendo la pelusa que solía estar dentro de la función fuera de la nueva clase), así que ahora El código bajo prueba que ejecuté antes toma diferentes parámetros bajo un nombre de clase diferente y produce diferentes resultados (el código original con la pelusa tenía más resultados para probar). Entonces, mis pruebas deben reflejar estos cambios y básicamente estoy reescribiendo mis pruebas en algo nuevo.
Supongo que hay otras soluciones que puedo hacer para evitar reescribir las pruebas. es decir, mantenga el nombre de la función anterior con un código nuevo y la pelusa dentro de él ... pero no sé si es la mejor idea y todavía no tengo tanta experiencia para decidir qué hacer.
fuente
Use sus pruebas para manejar su código mientras lo hace. En el código heredado, esto significa escribir pruebas para el código que va a cambiar. De esa manera no son un artefacto separado. Las pruebas deben ser sobre lo que el código necesita para lograr y no sobre las agallas internas de cómo lo hace.
En general, desea agregar pruebas en el código que no tiene ninguno) para el código que va a refactorizar para asegurarse de que el comportamiento de los códigos continúe funcionando según lo previsto. Por lo tanto, ejecutar continuamente el conjunto de pruebas mientras se refactoriza es una fantástica red de seguridad. La idea de cambiar el código sin un conjunto de pruebas para confirmar que los cambios no están afectando algo inesperado es aterrador.
En cuanto a lo esencial de actualizar las pruebas antiguas, escribir nuevas pruebas, eliminar pruebas antiguas, etc. Simplemente lo veo como parte del costo del desarrollo de software profesional moderno.
fuente
¿Cuál es el objetivo de refactorizar en su caso específico?
Presumimos con el propósito de soportar mi respuesta de que todos creemos (hasta cierto punto) en TDD (Test-Driven Development).
Si el propósito de su refactorización es limpiar el código existente sin cambiar el comportamiento existente, entonces escribir pruebas antes de refactorizar es cómo asegurarse de que no ha cambiado el comportamiento del código, si tiene éxito, las pruebas tendrán éxito tanto antes como después usted refactoriza.
Las pruebas lo ayudarán a asegurarse de que su nuevo trabajo realmente funcione.
Las pruebas probablemente también descubrirán casos en los que el trabajo original no funciona.
Pero, ¿cómo se hace una refactorización significativa sin afectar el comportamiento en algún grado?
Aquí hay una breve lista de algunas cosas que pueden suceder durante la refactorización:
Voy a argumentar que cada una de esas actividades enumeradas cambia el comportamiento de alguna manera.
Y voy a argumentar que si su refactorización cambia el comportamiento, sus pruebas seguirán siendo cómo se asegura de que no haya roto nada.
Quizás el comportamiento no cambie a nivel macro, pero el objetivo de las pruebas unitarias no es garantizar el comportamiento macro. Eso es prueba de integración . El objetivo de las pruebas unitarias es garantizar que las piezas y piezas individuales con las que construye su producto no se rompan. Cadena, eslabón más débil, etc.
¿Qué tal este escenario:
Presume que tienes
function bar()
function foo()
hace una llamada abar()
function flee()
también hace un llamado a funcionarbar()
Solo por variedad,
flam()
hace un llamado afoo()
Todo funciona espléndidamente (aparentemente, al menos).
Refactorizas ...
bar()
se renombra abarista()
flee()
se cambia para llamarbarista()
foo()
se no cambiado a la llamadabarista()
Obviamente, sus pruebas para ambos
foo()
yflam()
ahora fallan.Tal vez no te diste cuenta de que
foo()
llamastebar()
en primer lugar. Ciertamente no te diste cuenta de queflam()
dependía abar()
propósitofoo()
.Lo que sea. El punto es que sus pruebas descubrirán el comportamiento recientemente roto de ambos
foo()
yflam()
, de manera incremental, durante su trabajo de refactorización.Las pruebas terminan ayudándote a refactorizar bien.
A menos que no tengas ninguna prueba.
Ese es un poco un ejemplo artificial. Hay quienes argumentan que si cambiar los
bar()
descansosfoo()
,foo()
para empezar , sería demasiado complejo y debería desglosarse. Pero los procedimientos pueden llamar a otros procedimientos por una razón y es imposible eliminar toda la complejidad, ¿verdad? Nuestro trabajo es gestionar la complejidad razonablemente bien.Considere otro escenario.
Estás construyendo un edificio.
Construye un andamio para ayudar a garantizar que el edificio se construya correctamente.
El andamio lo ayuda a construir un pozo de ascensor, entre otras cosas. Después, derribas el andamio, pero el hueco del ascensor permanece. Has destruido el "trabajo original" al destruir el andamio.
La analogía es tenue, pero el punto es que no es inaudito construir herramientas que lo ayuden a construir productos. Incluso si las herramientas no son permanentes, son útiles (incluso necesarias). Los carpinteros hacen plantillas todo el tiempo, a veces solo para un trabajo. Luego desgarran las plantillas, a veces usan las piezas para construir otras plantillas para otros trabajos, a veces no. Pero eso no hace que las plantillas sean un esfuerzo inútil o desperdiciado.
fuente