He leído algunas respuestas a preguntas en una línea similar, como "¿Cómo mantiene funcionando las pruebas de su unidad cuando refactoriza?". En mi caso, el escenario es ligeramente diferente, ya que me han dado un proyecto para revisar y adaptarlo a algunos estándares que tenemos, ¡actualmente no hay pruebas para el proyecto!
He identificado varias cosas que creo que podrían haberse hecho mejor, como NO mezclar el código de tipo DAO en una capa de servicio.
Antes de refactorizar, parecía una buena idea escribir pruebas para el código existente. El problema que me parece es que cuando realizo una refactorización, esas pruebas se romperán a medida que cambie donde se realiza cierta lógica y las pruebas se escribirán teniendo en cuenta la estructura anterior (dependencias simuladas, etc.)
En mi caso, ¿cuál sería la mejor manera de proceder? Estoy tentado a escribir las pruebas alrededor del código refactorizado, pero sé que existe el riesgo de que pueda refactorizar cosas incorrectamente que podrían cambiar el comportamiento deseado.
Ya sea que se trate de una refactorización o un rediseño, estoy feliz de que comprenda que esos términos se deben corregir, actualmente estoy trabajando en la siguiente definición para refactorizar "Con la refactorización, por definición, no cambia lo que hace su software, cambias cómo lo hace ". Así que no estoy cambiando lo que hace el software, estaría cambiando cómo / dónde lo hace.
Igualmente puedo ver el argumento de que si estoy cambiando la firma de los métodos que podrían considerarse un rediseño.
Aquí hay un breve ejemplo
MyDocumentService.java
(actual)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(refactorizado / rediseñado lo que sea)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
fuente
Respuestas:
Estás buscando pruebas que comprueben las regresiones . es decir, romper algunos comportamientos existentes. Comenzaría por identificar a qué nivel ese comportamiento seguirá siendo el mismo, y que la interfaz que conduce ese comportamiento seguirá siendo el mismo, y comenzaría a realizar pruebas en ese punto.
Ahora tiene algunas pruebas que afirman que cualquier cosa que haga por debajo de este nivel, su comportamiento sigue siendo el mismo.
Tiene toda la razón al preguntar cómo las pruebas y el código pueden permanecer sincronizados. Si su interfaz con un componente sigue siendo la misma, puede escribir una prueba alrededor de esto y afirmar las mismas condiciones para ambas implementaciones (a medida que crea la nueva implementación). Si no es así, debe aceptar que una prueba para un componente redundante es una prueba redundante.
fuente
La práctica recomendada es comenzar por escribir "pruebas precisas" que prueben el comportamiento actual del código, posiblemente incluyendo errores, pero sin requerir que descienda a la locura de discernir si un comportamiento determinado que viola los documentos de requisitos es un error, solución para algo que no conoce, o que representa un cambio indocumentado en los requisitos.
Tiene más sentido que estas pruebas precisas sean de alto nivel, es decir, integración en lugar de pruebas unitarias, para que sigan funcionando cuando comience a refactorizar.
Pero algunas refactorizaciones pueden ser necesarias para que el código sea comprobable, solo tenga cuidado de apegarse a las refactorizaciones "seguras". Por ejemplo, en casi todos los casos, los métodos que son privados pueden hacerse públicos sin romper nada.
fuente
Sugiero, si aún no lo ha hecho, leer Trabajar eficazmente con código heredado y Refactorizar: Mejorar el diseño de código existente .
No necesariamente veo esto como un problema: escriba las pruebas, cambie la estructura de su código y luego ajuste la estructura de la prueba también . Esto le dará retroalimentación directa si su nueva estructura es realmente mejor que la anterior, porque si lo es, las pruebas ajustadas serán más fáciles de escribir (y, por lo tanto, cambiar las pruebas debería ser relativamente sencillo, lo que reduce el riesgo de tener una nueva introducción error pasa las pruebas).
Además, como ya han escrito otros: no escriba pruebas demasiado detalladas (al menos no al principio). Intente mantenerse en un alto nivel de abstracción (por lo tanto, sus pruebas probablemente se caracterizarán mejor como pruebas de regresión o incluso de integración).
fuente
No escriba pruebas unitarias estrictas donde se burle de todas las dependencias. Algunas personas te dirán que estas no son pruebas unitarias reales. Ingnóralos. Estas pruebas son útiles, y eso es lo que importa.
Veamos tu ejemplo:
Su prueba probablemente se parece a esto:
En lugar de burlarse de DocumentDao, burlarse de sus dependencias:
Ahora, puede mover la lógica de
MyDocumentService
adentroDocumentDao
sin romper las pruebas. Las pruebas mostrarán que la funcionalidad es la misma (en la medida en que la haya probado).fuente
Como usted dice, si cambia el comportamiento, entonces es una transformación y no un refactorizador. A qué nivel cambias el comportamiento es lo que hace la diferencia.
Si no hay pruebas formales al más alto nivel, intente encontrar un conjunto de requisitos que los clientes (código de llamada o humanos) deben permanecer igual después de su rediseño para que su código se considere que funciona. Esa es la lista de casos de prueba que necesita implementar.
Para responder a su pregunta sobre cambios en las implementaciones que requieren cambios en los casos de prueba, le sugiero que eche un vistazo a TDD Detroit (clásico) vs Londres (simulacro). Martin Fowler habla de esto en su gran artículo Los simulacros no son trozos, pero muchas personas tienen opiniones. Si comienza en el nivel más alto, donde sus elementos externos no pueden cambiar, y avanza hacia abajo, entonces los requisitos deben mantenerse bastante estables hasta llegar a un nivel que realmente necesita cambiar.
Sin ninguna prueba, esto será difícil, y es posible que desee considerar ejecutar clientes a través de rutas de código dual (y registrar las diferencias) hasta que pueda estar seguro de que su nuevo código hace exactamente lo que debe hacer.
fuente
Aquí mi acercamiento. Tiene un costo en términos de tiempo porque es una prueba de refactorización en 4 fases.
Lo que voy a exponer puede encajar mejor en componentes con más complejidad que el expuesto en el ejemplo de la pregunta.
De todos modos, la estrategia es válida para que cualquier candidato a componente sea normalizado por una interfaz (DAO, Servicios, Controladores, ...).
1. La interfaz
Vamos a reunir todos los métodos públicos de MyDocumentService y ponerlos todos juntos en una interfaz. Por ejemplo. Si ya existe, use ese en lugar de configurar uno nuevo .
Luego forzamos a MyDocumentService a implementar esta nueva interfaz.
Hasta aquí todo bien. No se hicieron cambios importantes, respetamos el contrato actual y behaivos permanece intacto.
2. Prueba unitaria del código heredado
Aquí tenemos el trabajo duro. Para configurar un conjunto de pruebas. Deberíamos establecer tantos casos como sea posible: casos exitosos y también casos de error. Estos últimos son por el bien de la calidad del resultado.
Ahora, en lugar de probar MyDocumentService , vamos a utilizar la interfaz como el contrato que se va a probar.
No voy a entrar en detalles, así que perdóname si mi código parece demasiado simple o demasiado agnóstico
Esta etapa lleva más tiempo que cualquier otra en este enfoque. Y es lo más importante porque establecerá el punto de referencia para futuras comparaciones.
Nota: Debido a que no se realizaron cambios importantes y el comportamiento permanece intacto. Sugiero hacer una etiqueta aquí en el SCM. La etiqueta o rama no importa. Solo haz una versión.
Lo queremos para retrocesos, comparaciones de versiones y puede ser para ejecuciones paralelas del código antiguo y el nuevo.
3. Refactorización
Refactor se implementará en un nuevo componente. No haremos ningún cambio en el código existente. El primer paso es tan fácil como copiar y pegar MyDocumentService y cambiarle el nombre a CustomDocumentService (por ejemplo).
Nueva clase sigue implementando DocumentService . Luego ve y refactoriza getAllDocuments () . (Comencemos por uno. Pin-refactores)
Puede requerir algunos cambios en la interfaz / métodos de DAO. Si es así, no cambie el código existente. Implemente su propio método en la interfaz DAO. Anote el código antiguo como obsoleto y sabrá más adelante qué debe eliminarse.
Es importante no romper / cambiar la implementación existente. Queremos ejecutar ambos servicios en paralelo y luego comparar los resultados.
4. Actualización de DocumentServiceTestSuite
Ok, ahora la parte más fácil. Para agregar las pruebas del nuevo componente.
Ahora tenemos oldResult y newResult, ambos validados independientemente, pero también podemos comparar entre nosotros. Esta última validación es opcional y depende del resultado. Puede ser que no sea comparable.
Puede que no parezca demasiado comparar dos colecciones de esta manera, pero sería válido para cualquier otro tipo de objeto (pojos, entidades de modelo de datos, DTO, envoltorios, tipos nativos ...)
Notas
No me atrevería a decir cómo hacer pruebas unitarias o cómo usar libs simuladas. Tampoco me atrevo a decir cómo tienes que hacer el refactor. Lo que quería hacer es sugerir una estrategia global. Cómo llevarlo adelante depende de ti. Sabes exactamente cómo es el código, su complejidad y si vale la pena intentarlo. Hechos como el tiempo y los recursos son importantes aquí. También importa qué espera de estas pruebas en el futuro.
Comencé mis ejemplos por un Servicio y seguiría con DAO y así sucesivamente. Profundizando en los niveles de dependencia. Más o menos podría describirse como una estrategia ascendente . Sin embargo, para cambios / refactores menores ( como el que se expone en el ejemplo del recorrido ), una tarea ascendente facilitaría la tarea. Porque el alcance de los cambios es pequeño.
Finalmente, depende de usted eliminar el código obsoleto y redirigir las dependencias antiguas al nuevo.
Elimine también las pruebas obsoletas y el trabajo está hecho. Si versionó la solución anterior con sus pruebas, puede verificar y comparar entre sí en cualquier momento.
Como consecuencia de tanto trabajo, ha probado, validado y versionado el código heredado. Y nuevo código, probado, validado y listo para ser versionado.
fuente
tl; dr No escriba pruebas unitarias. Escribir exámenes a un nivel más apropiado.
Dada su definición de trabajo de refactorización:
Hay un espectro muy amplio. En un extremo, hay un cambio autónomo en un método particular, tal vez usando un algoritmo más eficiente. En el otro extremo está portando a otro idioma.
Independientemente del nivel de refactorización / rediseño que se esté realizando, es importante tener pruebas que funcionen a ese nivel o superior.
Las pruebas automatizadas a menudo se clasifican por nivel como:
Pruebas unitarias : componentes individuales (clases, métodos)
Pruebas de integración : interacciones entre componentes
Pruebas del sistema : la aplicación completa
Escriba el nivel de prueba que puede soportar la refactorización esencialmente intacta.
Pensar:
fuente
No pierda el tiempo escribiendo pruebas que se conectan en puntos donde puede anticipar que la interfaz va a cambiar de una manera no trivial. Esto es a menudo una señal de que está tratando de evaluar las clases unitarias que son de naturaleza 'colaborativa', cuyo valor no está en lo que hacen ellos mismos, sino en cómo interactúan con una serie de clases estrechamente relacionadas para producir un comportamiento valioso . Es ese comportamiento el que desea probar, lo que significa que desea realizar la prueba a un nivel superior. Las pruebas por debajo de este nivel a menudo requieren mucha burla fea, y las pruebas resultantes pueden ser más una carga para el desarrollo que una ayuda para defender el comportamiento.
No se obsesione con si está haciendo un refactor, un rediseño o lo que sea. Puede realizar cambios que en el nivel inferior constituyen un rediseño de una serie de componentes, pero en un nivel de integración más alto simplemente equivalen a un refactorizador. El punto es tener claro qué comportamiento es valioso para usted y defender ese comportamiento a medida que avanza.
Puede ser útil tener en cuenta a medida que escribe sus pruebas: ¿podría describir fácilmente a un QA, al propietario de un producto o a un usuario lo que esta prueba realmente está probando? Si parece que describir la prueba sería demasiado esotérico y técnico, tal vez esté probando en el nivel incorrecto. Pruebe en los puntos / niveles que 'tengan sentido', y no estropee su código con pruebas en todos los niveles.
fuente
Su primera tarea es tratar de encontrar la "firma de método ideal" para sus pruebas. Esfuércese para que sea una función pura . Esto debería ser independiente del código que está realmente bajo prueba; Es una pequeña capa adaptadora. Escriba su código en esta capa de adaptador. Ahora, cuando refactorice su código, solo necesita cambiar la capa del adaptador. Aquí hay un ejemplo simple:
Las pruebas son buenas, pero el código bajo prueba tiene una API incorrecta. Puedo refactorizarlo sin cambiar las pruebas simplemente actualizando la capa de mi adaptador:
Este ejemplo parece algo bastante obvio según el principio de No repetirse, pero puede no ser tan obvio en otros casos. La ventaja va más allá de DRY: la ventaja real es el desacoplamiento de las pruebas del código bajo prueba.
Por supuesto, esta técnica puede no ser aconsejable en todas las situaciones. Por ejemplo, no habría razón para escribir adaptadores para POCO / POJO porque realmente no tienen una API que pueda cambiar independientemente del código de prueba. Además, si está escribiendo una pequeña cantidad de pruebas, una capa de adaptador relativamente grande probablemente sería un esfuerzo inútil.
fuente