¿Cómo mejorar drásticamente la cobertura del código?

21

Tengo la tarea de obtener una aplicación heredada bajo prueba unitaria. Primero algunos antecedentes sobre la aplicación: es una base de código RCP Java Java de 600k LOC con estos problemas principales

  • duplicación masiva de código
  • sin encapsulación, se puede acceder a la mayoría de los datos privados desde el exterior, algunos de los datos de la empresa también se hicieron únicos, por lo que no solo se pueden cambiar desde el exterior, sino también desde cualquier lugar.
  • sin abstracciones (por ejemplo, sin modelo comercial, los datos comerciales se almacenan en Object [] y double [] []), por lo que no hay OO.

Existe un buen conjunto de pruebas de regresión y un equipo de control de calidad eficiente está probando y encontrando errores. Conozco las técnicas para ponerlo a prueba en libros clásicos, por ejemplo, Michael Feathers, pero eso es demasiado lento. Como hay un sistema de prueba de regresión que funciona, no tengo miedo de refactorizar agresivamente el sistema para permitir que se escriban las pruebas unitarias.

¿Cómo debo comenzar a atacar el problema para obtener algo de cobertura rápidamente , de modo que pueda mostrar el progreso a la gerencia (y de hecho comenzar a ganar de la red de seguridad de las pruebas JUnit)? No quiero emplear herramientas para generar conjuntos de pruebas de regresión, por ejemplo, AgitarOne, porque estas pruebas no prueban si algo es correcto.

Peter Kofler
fuente
¿Por qué no crear las pruebas de regresión automáticamente y verificar cada una individualmente? Tiene que ser más rápido que escribirlos todos a mano.
Robert Harvey
Suena un poco extraño llamar a cualquier cosa escrita en el legado de Java, pero de acuerdo, ciertamente es un legado. Usted menciona que no tiene miedo de refactorizar el sistema para permitir que se escriban las pruebas unitarias, pero ¿no debería escribir las pruebas unitarias en el sistema tal cual, antes de intentar cualquier refactorización? ¿Entonces su refactorización puede ejecutarse a través de las mismas pruebas de unidades para garantizar que no se rompa nada?
dodgy_coder
1
@dodgy_coder Por lo general, de acuerdo, pero espero que el control de calidad tradicional que funciona de manera eficiente me ahorre algún tiempo.
Peter Kofler
1
@dodgy_coder Michael C. Feathers, autor de Working Effectively with Legacy code define el código Legacy como "código sin pruebas". Sirve como una definición útil.
StuperUser

Respuestas:

10

Creo que hay dos ejes principales a lo largo de los cuales se puede colocar el código cuando se trata de introducir pruebas unitarias: A) ¿qué tan comprobable es el código? y B) ¿qué tan estable es (es decir, con qué urgencia necesita pruebas)? Mirando solo a los extremos, esto produce 4 categorías:

  1. Código que es fácil de probar y quebradizo
  2. Código fácil de probar y estable
  3. Código que es difícil de probar y quebradizo
  4. Código que es difícil de probar y estable

La categoría 1 es el lugar obvio para comenzar, donde puede obtener muchos beneficios con relativamente poco trabajo. La categoría 2 le permite mejorar rápidamente su estadística de cobertura (buena para la moral) y obtener más experiencia con la base de código, mientras que la categoría 3 es un trabajo más (a menudo frustrante) pero también produce más beneficios. Lo que debe hacer primero depende de la importancia de las estadísticas de moral y cobertura para usted. Probablemente no valga la pena molestarse en la categoría 4.

Michael Borgwardt
fuente
Excelente. Tengo una idea de cómo determinar si es fácil verificar mediante análisis estático, por ejemplo, el recuento de dependencias / Testability Explorer. Pero, ¿cómo podría determinar si el código es frágil? No puedo hacer coincidir los defectos con unidades específicas (por ejemplo, clases), y si es el número 3, por supuesto (clases de Dios / singletons). Entonces, ¿tal vez el número de registros (los puntos de acceso)?
Peter Kofler
1
@ Peter Kofler: los hotspots de commit son una buena idea, pero la fuente más valiosa de este tipo de conocimiento serían los desarrolladores que han trabajado con el código.
Michael Borgwardt
1
@Peter: como dijo Michael, los desarrolladores que han trabajado con el código. Cualquiera que haya trabajado con una gran base de código durante un buen tiempo sabrá a qué partes huele. O, si todo huele, qué partes realmente apestan .
Carson63000
15

Tengo mucha experiencia trabajando en sistemas heredados (no Java), mucho más grande que esto. Odio ser portador de malas noticias, tu problema es el tamaño de tu problema. Sospecho que lo has subestimado.

Agregar pruebas de regresión al código heredado es un proceso lento y costoso. Muchos requisitos no están bien documentados: una corrección de errores aquí, un parche allí, y antes de que se dé cuenta, el software define su propio comportamiento. No tener pruebas significa que la implementación es todo lo que hay que pasar, no hay pruebas para "desafiar" los requisitos implícitos implementados en el código.

Si intenta obtener cobertura rápidamente, es probable que apresure el trabajo, lo hornee a medias y falle. Las pruebas proporcionarán una cobertura parcial de las cosas obvias y una cobertura deficiente o nula de los problemas reales. Usted convencerá a los mismos gerentes que está tratando de vender a esa Unidad de Pruebas que no vale la pena, que es solo otra bala de plata que no funciona.

En mi humilde opinión, el mejor enfoque es apuntar a las pruebas. Utilice las métricas, la intuición y los informes de registro de defectos para identificar el 1% o el 10% del código que produce la mayoría de los problemas. Golpea estos módulos con fuerza e ignora el resto. No intentes hacer demasiado, menos es más.

Un objetivo realista es "Desde que implementamos UT, la inserción de defectos en los módulos bajo prueba se ha reducido a x% de aquellos que no están bajo UT" (idealmente, x es un número <100).

Mattnz
fuente
+1, no puedes probar algo de manera efectiva sin un estándar más fuerte que el código.
dan_waterworth
Lo sé y estoy de acuerdo. La diferencia es que tenemos pruebas, pruebas de regresión tradicionales trabajando QA en su lugar, por lo que hay algún tipo de red de seguridad. En segundo lugar, estoy totalmente a favor de las pruebas unitarias, por lo que definitivamente no será otra cosa que no funcionó. Un buen punto sobre a qué apuntar primero. Gracias.
Peter Kofler el
1
y no olvide que el solo hecho de buscar "cobertura" no mejorará la calidad, ya que se quedará atrapado en una avalancha de pruebas imperfectas y triviales (y pruebas de código trivial que no necesitan pruebas explícitas, pero se agregan solo para aumentar la cobertura). Terminará creando pruebas para complacer la herramienta de cobertura, no porque sean útiles, y posiblemente va a cambiar el código en sí para aumentar la cobertura de la prueba sin escribir pruebas (como cortar comentarios y definiciones variables, que algunas herramientas de cobertura llamará al código descubierto).
Jwenting 05 de
2

Me recuerda ese dicho acerca de no preocuparse por la puerta del granero cuando el caballo ya se ha escapado.

La realidad es que realmente no hay una forma rentable de obtener una buena cobertura de prueba para un sistema heredado, ciertamente no en un corto período de tiempo. Como MattNz mencionó, será un proceso que llevará mucho tiempo y, en última instancia, será muy costoso. Mi instinto me dice que si intentas hacer algo para impresionar a la administración, es probable que crees una nueva pesadilla de mantenimiento al tratar de mostrar demasiado demasiado rápido, sin comprender completamente los requisitos que estás intentando probar.

Siendo realistas, una vez que ya ha escrito el código, es casi demasiado tarde para escribir las pruebas de manera efectiva sin el riesgo de perder algo vital. Por otro lado, se podría decir que algunas pruebas son mejores que ninguna, pero si ese es el caso, las pruebas mismas deben mostrar que agregan valor al sistema en su conjunto.

Mi sugerencia sería mirar esas áreas clave donde sientes que algo está "roto". Con eso quiero decir que podría ser un código terriblemente ineficiente, o que puedes demostrar que anteriormente era costoso de mantener. Documente los problemas, luego use esto como punto de partida para introducir un nivel de prueba que lo ayude a mejorar el sistema, sin embarcarse en un esfuerzo masivo de reingeniería. La idea aquí es evitar ponerse al día con las pruebas y, en su lugar, introducir pruebas para ayudarlo a resolver problemas específicos. Después de un período de tiempo, vea si puede medir y distinguir entre el costo anterior de mantener esa sección de código y los esfuerzos actuales con las correcciones que ha aplicado con sus pruebas de soporte.

Lo que debe recordar es que la administración está más interesada en el costo / beneficio y cómo eso afecta directamente a sus clientes y, en última instancia, a su presupuesto. Nunca están interesados ​​en hacer algo simplemente porque es lo mejor que se puede hacer a menos que pueda demostrar que les proporcionará un beneficio que les interese. Si puede demostrar que está mejorando el sistema y obteniendo una buena cobertura de prueba para el trabajo que está haciendo actualmente, es más probable que la administración vea esto como una aplicación eficiente de sus esfuerzos. Esto podría permitirle argumentar el caso para extender sus esfuerzos a otras áreas clave, sin exigir una congelación completa del desarrollo del producto o, lo que es peor, ¡casi imposible argumentar a favor de reescribir!

S.Robins
fuente
1

Una forma de mejorar la cobertura es escribir más pruebas.

Otra forma es reducir la redundancia en su código, de tal manera que las pruebas existentes en efecto cubran el código redundante que actualmente no está cubierto.

Imagine que tiene 3 bloques de código, a, by b ', donde b' es un duplicado (copia exacta o casi incorrecta) de B, y que tiene cobertura en ayb pero no b 'con la prueba T.

Si refactoriza la base del código para eliminar b 'extrayendo la comunidad de b y b' como B, la base del código ahora se ve como a, b0, B, b'0, con b0 que contiene el código no compartido con b'0 y viceversa. viceversa, y tanto b0 como b'0 son mucho más pequeños que B e invocan / usan B.

Ahora la funcionalidad del programa no ha cambiado, y tampoco la prueba T, por lo que podemos ejecutar T nuevamente y esperar que pase. El código ahora cubierto es a, b0 y B, pero no b'0. La base del código se ha vuelto más pequeña (¡b'0 es más pequeña que b '!) Y aún cubrimos lo que cubrimos originalmente. Nuestro índice de cobertura ha aumentado.

Para hacer esto, necesita encontrar b, b 'e idealmente B para habilitar su refactorización. Nuestra herramienta CloneDR puede hacer esto para muchos idiomas, especialmente Java. Dices que tu código base tiene muchos códigos duplicados; Esta podría ser una buena manera de abordarlo en su beneficio.

Curiosamente, el acto de encontrar byb 'a menudo aumenta su vocabulario sobre las ideas abstractas que implementa el programa. Si bien la herramienta no tiene idea de lo que hacen byb ', el acto mismo de aislarlos del código, lo que permite un enfoque simple en el contenido de byb', a menudo les da a los programadores una muy buena idea de qué abstracción B_abstract implementa el código clonado . Por lo tanto, esto también mejora su comprensión del código. Asegúrate de darle un buen nombre a B cuando lo abstienes, y obtendrás una mejor cobertura de prueba y un programa más fácil de mantener.

Ira Baxter
fuente