¿Qué ayudaría al refactorizar un método grande para garantizar que no rompa nada?

10

Actualmente estoy refactorizando una parte de una gran base de código sin pruebas unitarias de ningún tipo. Traté de refactorizar el código de forma bruta, es decir, tratando de adivinar qué está haciendo el código y qué cambios no cambiarían su significado, pero sin éxito: rompe aleatoriamente las características de todo el código base.

Tenga en cuenta que la refactorización incluye mover el código C # heredado a un estilo más funcional (el código heredado no usa ninguna de las características de .NET Framework 3 y posteriores, incluido LINQ), agregando genéricos donde el código puede beneficiarse de ellos, etc.

No puedo usar métodos formales , dado cuánto costarían.

Por otro lado, supongo que al menos la regla "Cualquier código heredado refactorizado vendrá con pruebas unitarias" debe seguirse estrictamente, sin importar cuánto costaría. El problema es que cuando refactorizo ​​una pequeña parte de un método privado de 500 LOC, agregar pruebas unitarias parece ser una tarea difícil.

¿Qué puede ayudarme a saber qué pruebas unitarias son relevantes para un determinado código? Supongo que el análisis estático del código de alguna manera sería útil, pero ¿cuáles son las herramientas y técnicas que puedo usar para:

  • Sepa exactamente qué pruebas unitarias debo crear,

  • ¿Y / o sabe si el cambio que hice afectó el código original de una manera que se ejecuta de manera diferente a la actual?

Arseni Mourzenko
fuente
¿Cuál es su razonamiento de que escribir pruebas unitarias aumentará el tiempo para este proyecto? Muchos proponentes no estarían de acuerdo, pero también depende de su capacidad para escribirlos.
JeffO
No digo que aumentará el tiempo total para el proyecto. Lo que quería decir es que aumentará el tiempo a corto plazo (es decir, el tiempo inmediato que paso ahora al refactorizar el código).
Arseni Mourzenko
1
De formal methods in software developmenttodos modos , no querría usarlo porque se usa para probar la exactitud de un programa usando lógica de predicados y no tendría aplicabilidad para refactorizar una base de código grande. Los métodos formales que generalmente se usan para probar el código funcionan correctamente en áreas como aplicaciones médicas. Tiene razón, es costoso hacerlo, por eso no se usa con frecuencia.
Mushy
Una buena herramienta, como las opciones de refactorización en ReSharper, hacen que dicha tarea sea mucho más fácil. En situaciones como esta, vale la pena el dinero.
billy.bob
1
No es una respuesta completa, sino una técnica tonta que encuentro sorprendentemente efectiva cuando todas las demás técnicas de refactorización me fallan: crear una nueva clase, dividir la función en funciones separadas con el código que ya existe, simplemente romper cada 50 líneas más o menos, promover cualquier locales que se comparten entre las funciones con los miembros, luego las funciones individuales se ajustan mejor a mi cabeza y me dan la capacidad de ver en los miembros qué piezas se entrelazan a través de toda la lógica. Este no es un objetivo final, solo una forma segura de obtener un desorden heredado para estar listo para refactorizar de manera segura.
Jimmy Hoffa

Respuestas:

12

He tenido desafíos similares. El libro Working with Legacy Code es un gran recurso, pero se asume que puedes calzar zapatos en pruebas unitarias para apoyar tu trabajo. A veces eso simplemente no es posible.

En mi trabajo de arqueología (mi término para mantenimiento en código heredado como este), sigo un enfoque similar en cuanto a lo que usted describió.

  • Comience con una comprensión sólida de lo que la rutina está haciendo actualmente.
  • Al mismo tiempo, identifique lo que se suponía que debía hacer la rutina . Muchos piensan que esta bala y la anterior son iguales, pero hay una sutil diferencia. Muchas veces, si la rutina estaba haciendo lo que se suponía que debía hacer, no estaría aplicando cambios de mantenimiento.
  • Ejecute algunas muestras a través de la rutina y asegúrese de tocar los casos límite, las rutas de error relevantes, junto con la ruta de la línea principal. Mi experiencia es que el daño colateral (ruptura de la característica) proviene de condiciones límite que no se implementan exactamente de la misma manera.
  • Después de esos casos de muestra, identifique lo que persiste que no necesariamente necesita persistir. Nuevamente, he descubierto que son los efectos secundarios como este los que provocan daños colaterales en otros lugares.

En este punto, debe tener una lista de candidatos de lo que ha sido expuesto y / o manipulado por esa rutina. Es probable que algunas de esas manipulaciones sean involuntarias. Ahora uso findstry el IDE para comprender qué otras áreas pueden hacer referencia a los elementos en la lista de candidatos. Pasaré un tiempo entendiendo cómo funcionan esas referencias y cuál es su naturaleza.

Finalmente, una vez que me engañe pensando que entiendo los impactos de la rutina original, haré mis cambios uno por uno y volveré a ejecutar los pasos de análisis que describí anteriormente para verificar que el cambio esté funcionando como esperaba a trabajar Trato específicamente de evitar cambiar varias cosas a la vez, ya que descubrí que esto me afecta cuando trato de verificar el impacto. A veces puede salirse con la suya con múltiples cambios, pero si puedo seguir una ruta de uno en uno, esa es mi preferencia.

En resumen, mi enfoque es similar a lo que usted presentó. Es mucho trabajo de preparación; luego haga cambios circunspectos e individuales; y luego verificar, verificar, verificar.


fuente
2
+1 para el uso de "arqueología" solo. Ese es el mismo término que uso para describir esta actividad y creo que es una excelente manera de decirlo (también pensé que la respuesta era buena, no soy tan superficial)
Erik Dietrich
10

¿Qué ayudaría al refactorizar un método grande para garantizar que no rompa nada?

Respuesta corta: pequeños pasos.

El problema es que cuando refactorizo ​​una pequeña parte de un método privado de 500 LOC, agregar pruebas unitarias parece ser una tarea difícil.

Considere estos pasos:

  1. Mueva la implementación a una función diferente (privada) y delegue la llamada.

    // old:
    private int ugly500loc(int parameters) {
        // 500 LOC here
    }
    
    // new:    
    private int ugly500loc_old(int parameters) {
        // 500 LOC here
    }
    
    private void ugly500loc(int parameters) {
        return ugly500loc_old(parameters);
    }
    
  2. Agregue código de registro (asegúrese de que el registro no falle) en su función original, para todas las entradas y salidas.

    private void ugly500loc(int parameters) {
        static int call_count = 0;
        int current = ++call_count;
        save_to_file(current, parameters);
        int result = ugly500loc_old(parameters);
        save_to_file(current, result); // result, any exceptions, etc.
        return result;
    }
    

    Ejecute su aplicación y haga lo que pueda con ella (uso válido, uso no válido, uso típico, uso atípico, etc.).

  3. Ahora tiene max(call_count)conjuntos de entradas y salidas para escribir sus pruebas; Podría escribir una sola prueba que repita todos sus parámetros / conjuntos de resultados que tenga y los ejecute en un bucle. También puede escribir una prueba adicional que ejecute una combinación particular (que se utilizará para verificar rápidamente el paso sobre un conjunto de E / S en particular).

  4. Mover // 500 LOC herede nuevo en su ugly500locfunción (y eliminar la funcionalidad de registro).

  5. Comience a extraer funciones de la función grande (no haga nada más, solo extraiga funciones) y ejecute pruebas. Después de esto, debería tener más pequeñas funciones para refactorizar, en lugar de la 500LOC.

  6. Vivir feliz para siempre.

utnapistim
fuente
3

Por lo general, las pruebas unitarias son el camino a seguir.

Realice las pruebas necesarias que prueben que la corriente funciona como se esperaba Tómese su tiempo y la última prueba debe hacerle confiar en la salida.

¿Qué puede ayudarme a saber qué pruebas unitarias son relevantes para un determinado código?

Estás en el proceso de refactorizar un fragmento de código, debes saber exactamente qué hace y qué impacto tiene. Básicamente, necesitas probar todas las zonas afectadas. Esto le llevará mucho tiempo ... pero ese es un resultado esperado de cualquier proceso de refactorización.

Entonces puedes desgarrar todo sin problemas.

AFAIK, no hay una técnica a prueba de balas para esto ... ¡solo debes ser metódico (en cualquier método con el que te sientas cómodo), mucho tiempo y mucha paciencia! :)

Saludos y buena suerte!

Alex

AlexCode
fuente
Las herramientas de cobertura de código son esenciales aquí. Es difícil confirmar que ha cubierto todos los caminos a través de un método complejo grande mediante inspección. Una herramienta que muestra que colectivamente KitchenSinkMethodTest01 () ... KitchenSinkMethodTest17 () cubren las líneas 1-45, 48-220, 245-399 y 488-500 pero no tocan el código entre; hará que descubrir qué pruebas adicionales necesita para escribir sea mucho más simple.
Dan Is Fiddling By Firelight