He estado luchando con un problema cada vez más molesto con respecto a nuestras pruebas unitarias que estamos implementando en mi equipo. Estamos intentando agregar pruebas unitarias en el código heredado que no fue bien diseñado y, aunque no hemos tenido ninguna dificultad con la adición real de las pruebas, estamos empezando a tener dificultades con la forma en que se están desarrollando las pruebas.
Como ejemplo del problema, supongamos que tiene un método que llama a otros 5 métodos como parte de su ejecución. Una prueba para este método podría ser confirmar que se produce un comportamiento como resultado de uno de estos otros 5 métodos llamados. Entonces, debido a que una prueba unitaria debe fallar por una razón y solo por una, desea eliminar los problemas potenciales causados al llamar a estos otros 4 métodos y simularlos. ¡Excelente! La prueba unitaria se ejecuta, los métodos simulados se ignoran (y su comportamiento puede confirmarse como parte de otras pruebas unitarias) y la verificación funciona.
Pero hay un nuevo problema: la prueba unitaria tiene un conocimiento íntimo de cómo confirmó que el comportamiento y cualquier cambio de firma a cualquiera de esos otros 4 métodos en el futuro, o cualquier método nuevo que deba agregarse al 'método principal', resultar en tener que cambiar la prueba de la unidad para evitar posibles fallas.
Naturalmente, el problema podría mitigarse un poco simplemente haciendo que más métodos logren menos comportamientos, pero esperaba que tal vez hubiera una solución más elegante disponible.
Aquí hay una prueba de unidad de ejemplo que captura el problema.
Como nota rápida, 'MergeTests' es una clase de prueba unitaria que hereda de la clase que estamos probando y anula el comportamiento según sea necesario. Este es un "patrón" que empleamos en nuestras pruebas para permitirnos anular llamadas a clases / dependencias externas.
[TestMethod]
public void VerifyMergeStopsSpinner()
{
var mockViewModel = new Mock<MergeTests> { CallBase = true };
var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());
mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
mockViewModel.Setup(
m =>
m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
It.IsAny<bool>()));
mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
mockViewModel.Setup(m => m.SwitchToOverviewTab());
mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));
mockViewModel.Object.OnMerge(It.IsAny<MergeState>());
mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}
¿Cómo han lidiado con esto el resto de ustedes o no hay una gran forma 'simple' de manejarlo?
Actualización: agradezco los comentarios de todos. Desafortunadamente, y no es una sorpresa realmente, no parece haber una gran solución, patrón o práctica que uno pueda seguir en las pruebas unitarias si el código que se prueba es deficiente. Marqué la respuesta que mejor capturó esta simple verdad.
fuente
Respuestas:
Arregle el código para estar mejor diseñado. Si sus pruebas tienen estos problemas, su código tendrá problemas peores cuando intente cambiar las cosas.
Si no puedes, entonces quizás debas ser menos ideal. Pruebe contra las condiciones previas y posteriores del método. ¿A quién le importa si estás usando los otros 5 métodos? Presumiblemente tienen sus propias pruebas unitarias, lo que deja en claro (er) qué causó la falla cuando fallan las pruebas.
"las pruebas unitarias deben tener una sola razón para fallar" es una buena guía, pero en mi experiencia, poco práctica. Las pruebas difíciles de escribir no se escriben. Las pruebas frágiles no se creen.
fuente
Romper los métodos grandes en métodos pequeños más enfocados es definitivamente una mejor práctica. Lo ve como dolor al verificar el comportamiento de la prueba de unidad, pero también está experimentando el dolor de otras maneras.
Dicho esto, es una herejía, pero personalmente soy fanático de crear entornos de prueba temporales realistas. Es decir, en lugar de burlarse de todo lo que está oculto dentro de esos otros métodos, asegúrese de que haya un entorno temporal fácil de configurar (completo con bases de datos y esquemas privados; SQLite puede ayudar aquí) que le permite ejecutar todas esas cosas. La responsabilidad de saber cómo construir / destruir ese entorno de prueba reside en el código que lo requiere, de modo que cuando cambie, no tenga que cambiar todo el código de prueba de la unidad que dependía de su existencia.
Pero sí noto que esta es una herejía de mi parte. Las personas que están muy interesadas en las pruebas unitarias abogan por las pruebas unitarias "puras" y llaman a lo que describí "pruebas de integración". Personalmente no me preocupa esa distinción.
fuente
Consideraría simplificar los simulacros y simplemente formular pruebas que podrían incluir los métodos a los que llama.
No pruebes el cómo , prueba el qué . Lo que importa es el resultado, incluya los submétodos si es necesario.
Desde otro ángulo, podría formular una prueba, hacerla pasar con un método grande, refactorizar y terminar con un árbol de métodos después de la refactorización. No necesita probar todos y cada uno de ellos de forma aislada. Es el resultado final lo que cuenta.
Si los métodos secundarios hacen que sea difícil probar algunos aspectos, considere dividirlos en clases separadas para que pueda burlarse de ellos de manera más limpia sin que su clase bajo prueba esté muy instrumentada / ajustada. Es un poco difícil saber si realmente está probando alguna implementación concreta en su prueba de ejemplo.
fuente