Por lo general, trato de seguir los consejos del libro Working Effectively with Legacy Cod e . Rompo dependencias, muevo partes del código a @VisibleForTesting public static
métodos y a nuevas clases para hacer que el código (o al menos una parte de él) sea comprobable. Y escribo pruebas para asegurarme de no romper nada cuando estoy modificando o agregando nuevas funciones.
Un colega dice que no debería hacer esto. Su razonamiento:
- El código original podría no funcionar correctamente en primer lugar. Y escribir pruebas para ello hace que las futuras correcciones y modificaciones sean más difíciles, ya que los desarrolladores también deben comprender y modificar las pruebas.
- Si es código GUI con alguna lógica (~ 12 líneas, 2-3 bloque if / else, por ejemplo), una prueba no merece la pena ya que el código es demasiado trivial para empezar.
- También podrían existir patrones incorrectos similares en otras partes de la base de código (que aún no he visto, soy bastante nuevo); será más fácil limpiarlos todos en una gran refactorización. Extraer la lógica podría socavar esta posibilidad futura.
¿Debo evitar extraer partes comprobables y escribir pruebas si no tenemos tiempo para refactorizar por completo? ¿Hay alguna desventaja en esto que debería considerar?
Respuestas:
Aquí está mi impresión personal no científica: las tres razones suenan como ilusiones cognitivas generalizadas pero falsas.
fuente
Algunas reflexiones:
Cuando refactoriza el código heredado, no importa si algunas de las pruebas que escribe contradicen las especificaciones ideales. Lo que importa es que prueben el comportamiento actual del programa . La refactorización consiste en tomar pequeños pasos iso-funcionales para hacer que el código sea más limpio; no desea participar en la corrección de errores mientras está refactorizando. Además, si detecta un error evidente, no se perderá. Siempre puede escribir una prueba de regresión y deshabilitarla temporalmente, o insertar una tarea de corrección de errores en su cartera de pedidos para más adelante. Una cosa a la vez.
Estoy de acuerdo en que el código GUI puro es difícil de probar y quizás no sea una buena opción para la refactorización de estilo " Trabajando efectivamente ... ". Sin embargo, esto no significa que no deba extraer un comportamiento que no tiene nada que ver en la capa GUI y probar el código extraído. Y "12 líneas, 2-3 si / sino bloque" no es trivial. Se debe probar todo el código con al menos un poco de lógica condicional.
En mi experiencia, las grandes refactorizaciones no son fáciles y rara vez funcionan. Si no te propones metas pequeñas y precisas, existe un alto riesgo de que te embarques en un reproceso interminable y agotador donde nunca terminarás de pie. Cuanto mayor sea el cambio, más te arriesgas a romper algo y más problemas tendrás para descubrir dónde fallaste.
Mejorar progresivamente las cosas con pequeñas refactorizaciones ad hoc no está "socavando las posibilidades futuras", las está habilitando, solidificando el terreno pantanoso donde se encuentra su aplicación. Definitivamente deberías hacerlo.
fuente
También re: "El código original podría no funcionar correctamente", eso no significa que simplemente cambie el comportamiento del código sin preocuparse por el impacto. Otro código puede depender de lo que parece ser un comportamiento roto o efectos secundarios de la implementación actual. La cobertura de prueba de la aplicación existente debería facilitar la refactorización posterior, ya que te ayudará a descubrir cuándo has roto algo accidentalmente. Debes probar las partes más importantes primero.
fuente
La respuesta de Kilian cubre los aspectos más importantes, pero quiero ampliar los puntos 1 y 3.
Si un desarrollador quiere cambiar (refactorizar, ampliar, depurar) el código, debe entenderlo. Debe asegurarse de que sus cambios afecten exactamente el comportamiento que desea (nada en el caso de refactorización) y nada más.
Si hay pruebas, entonces ella también tiene que entender las pruebas, claro. Al mismo tiempo, las pruebas deberían ayudarla a comprender el código principal, y las pruebas son mucho más fáciles de entender que el código funcional de todos modos (a menos que sean malas pruebas). Y las pruebas ayudan a mostrar qué cambió en el comportamiento del antiguo código. Incluso si el código original es incorrecto y la prueba prueba ese comportamiento incorrecto, sigue siendo una ventaja.
Sin embargo, esto requiere que las pruebas estén documentadas como pruebas de comportamiento preexistente, no como una especificación.
Algunas reflexiones sobre el punto 3: además del hecho de que el "gran golpe" rara vez ocurre, también hay otra cosa: en realidad no es más fácil. Para ser más fácil, tendrían que aplicarse varias condiciones:
XYZSingleton
? ¿Se llama siempre a su instancia gettergetInstance()
? ¿Y cómo encuentras tus jerarquías demasiado profundas? ¿Cómo buscas tus objetos divinos? Estos requieren un análisis de métricas de código y luego inspeccionar manualmente las métricas. O simplemente te tropiezas con ellos mientras trabajas, como lo hiciste.fuente
Hay una cultura en algunas compañías donde son reticentes a permitir a los desarrolladores en cualquier momento mejorar el código que no entrega directamente valor adicional, por ejemplo, nueva funcionalidad.
Probablemente estoy predicando a los convertidos aquí, pero eso es claramente una economía falsa. El código limpio y conciso beneficia a los desarrolladores posteriores. Es solo que la recuperación no es evidente de inmediato.
Me suscribo personalmente al Principio de Boy Scout, pero otros (como has visto) no lo hacen.
Dicho esto, el software sufre de entropía y acumula deuda técnica. Los desarrolladores anteriores con poco tiempo (o tal vez solo perezosos o inexpertos) pueden haber implementado soluciones de errores subóptimas sobre soluciones bien diseñadas. Si bien puede parecer deseable refactorizarlos, corre el riesgo de introducir nuevos errores en el código de trabajo (para los usuarios de todos modos).
Algunos cambios son de menor riesgo que otros. Por ejemplo, donde trabajo tiende a haber una gran cantidad de código duplicado que se puede cultivar de forma segura en una subrutina con un impacto mínimo.
En última instancia, debe tomar una decisión de juicio sobre cuán lejos lleva la refactorización, pero hay un valor innegable en agregar pruebas automatizadas si aún no existen.
fuente
En mi experiencia, una prueba de caracterización de algún tipo funciona bien. Le brinda una cobertura de prueba amplia pero no muy específica con relativa rapidez, pero puede ser difícil de implementar para aplicaciones GUI.
Luego escribiría pruebas unitarias para las partes que desea cambiar y lo haría cada vez que quiera hacer un cambio, aumentando así la cobertura de la prueba unitaria con el tiempo.
Este enfoque le da una buena idea si los cambios están afectando a otras partes del sistema y le permite estar en condiciones de realizar los cambios necesarios antes.
fuente
Re: "El código original podría no funcionar correctamente":
Las pruebas no están escritas en piedra. Se pueden cambiar. Y si probó una característica que estaba mal, debería ser fácil reescribir la prueba más correctamente. Solo el resultado esperado de la función probada debería haber cambiado, después de todo.
fuente
Bueno, sí. Respondiendo como ingeniero de pruebas de software. En primer lugar, debes probar todo lo que haces de todos modos. Porque si no lo haces, no sabes si funciona o no. Esto puede parecer obvio para nosotros, pero tengo colegas que lo ven de manera diferente. Incluso si su proyecto es pequeño y puede que nunca se entregue, debe mirar al usuario a la cara y decir que sabe que funciona porque lo probó.
El código no trivial siempre contiene errores (citando a un chico de la universidad; y si no hay errores, es trivial) y nuestro trabajo es encontrarlos antes que el cliente. El código heredado tiene errores heredados. Si el código original no funciona como debería, quieres saberlo, créeme. Los errores están bien si los conoce, no tenga miedo de encontrarlos, para eso están las notas de la versión.
Si recuerdo correctamente, el libro de Refactorización dice que debe probar constantemente de todos modos, por lo que es parte del proceso.
fuente
Haga la cobertura de prueba automatizada.
Tenga cuidado con las ilusiones, tanto propias como de sus clientes y jefes. Por mucho que me gustaría creer que mis cambios serán correctos la primera vez y solo tendré que probarlo una vez, he aprendido a tratar ese tipo de pensamiento de la misma manera que trato los correos electrónicos de estafa nigerianos. Bueno, sobre todo; Nunca he ido por un correo electrónico fraudulento, pero recientemente (cuando me gritaron) me di por vencido al no usar las mejores prácticas. Fue una experiencia dolorosa que se prolongó (costosamente) una y otra vez. ¡Nunca más!
Tengo una cita favorita del cómic web de Freefall: "¿Alguna vez ha trabajado en un campo complejo donde el supervisor solo tiene una idea aproximada de los detalles técnicos? ... Entonces sabe que la forma más segura de hacer que su supervisor falle es siga cada una de sus órdenes sin dudar ".
Probablemente sea apropiado limitar la cantidad de tiempo que invierte.
fuente
Si está lidiando con grandes cantidades de código heredado que no se está probando actualmente, obtener la cobertura de prueba ahora en lugar de esperar una reescritura hipotética en el futuro es el movimiento correcto. Comenzar escribiendo pruebas unitarias no lo es.
Sin pruebas automáticas, después de realizar cualquier cambio en el código, debe realizar algunas pruebas manuales de extremo a extremo de la aplicación para asegurarse de que funciona. Comience escribiendo pruebas de integración de alto nivel para reemplazar eso. Si su aplicación lee archivos, los valida, procesa los datos de alguna manera y muestra los resultados que desea pruebas que capturan todo eso.
Idealmente, tendrá datos de un plan de prueba manual o podrá obtener una muestra de datos de producción reales para usar. Si no es así, dado que la aplicación está en producción, en la mayoría de los casos está haciendo lo que debería ser, así que solo invente datos que lleguen a todos los puntos altos y suponga que la salida es correcta por ahora. No es peor que tomar una función pequeña, suponiendo que está haciendo lo que su nombre o cualquier comentario sugiere que debería estar haciendo, y escribir pruebas asumiendo que funciona correctamente.
Una vez que tenga suficientes de estas pruebas de alto nivel escritas para capturar el funcionamiento normal de las aplicaciones y los casos de error más comunes, la cantidad de tiempo que necesitará para golpear el teclado para tratar de detectar errores del código haciendo algo diferente a lo que pensaste que se suponía que debía hacerlo, se reduciría significativamente, lo que facilitaría mucho la refactorización futura (o incluso una gran reescritura).
Como puede ampliar la cobertura de pruebas unitarias, puede reducir o incluso retirar la mayoría de las pruebas de integración. Si los archivos de lectura / escritura de su aplicación o el acceso a una base de datos, probar estas partes de forma aislada y burlarse de ellas o hacer que comiencen sus pruebas creando las estructuras de datos leídas del archivo / base de datos es un lugar obvio para comenzar. En realidad, crear esa infraestructura de prueba llevará mucho más tiempo que escribir un conjunto de pruebas rápidas y sucias; y cada vez que ejecuta un conjunto de pruebas de integración de 2 minutos en lugar de pasar 30 minutos probando manualmente una fracción de lo que cubren las pruebas de integración, ya está obteniendo una gran victoria.
fuente