Tengo una clase que se refactoriza en 1 clase principal y 2 clases más pequeñas. Las clases principales usan la base de datos (como hacen muchas de mis clases) y envían un correo electrónico. Entonces, la clase principal tiene un IPersonRepository
y un IEmailRepository
inyectado que a su vez envía a las 2 clases más pequeñas.
Ahora quiero hacer una prueba unitaria de la clase principal, y he aprendido a no hacer una prueba unitaria del funcionamiento interno de la clase, porque deberíamos poder cambiar el funcionamiento interno sin interrumpir las pruebas unitarias.
Pero como la clase usa el IPersonRepository
y an IEmailRepository
, DEBO especificar resultados (simulados / ficticios) para algunos métodos para el IPersonRepository
. La clase principal calcula algunos datos basados en datos existentes y los devuelve. Si quiero probar eso, no veo cómo puedo escribir una prueba sin especificar que IPersonRepository.GetSavingsByCustomerId
devuelve x. Pero entonces mi prueba unitaria 'sabe' sobre el funcionamiento interno, porque 'sabe' qué métodos burlarse y cuáles no.
¿Cómo puedo probar una clase que ha inyectado dependencias, sin que la prueba conozca las partes internas?
antecedentes:
En mi experiencia, muchas pruebas como esta crean simulacros para los repositorios y luego proporcionan los datos correctos para las simulaciones o prueban si se llamó a un método específico durante la ejecución. De cualquier manera, la prueba conoce los aspectos internos.
Ahora he visto una presentación sobre la teoría (que he escuchado antes) de que la prueba no debe saber sobre la implementación. Primero porque no está probando cómo funciona, sino también porque cuando ahora cambia la implementación, todas las pruebas unitarias fallan porque 'saben' sobre la implementación. Si bien me gusta que el concepto de las pruebas desconozca la implementación, no sé cómo lograrlo.
fuente
IPersonRepository
objeto, esa interfaz y todos los métodos que describe ya no son "internos", por lo que en realidad no es un problema de la prueba. Su verdadera pregunta debería ser "¿cómo puedo refactorizar las clases en unidades más pequeñas sin exponer demasiado en público". La respuesta es "mantener esas interfaces esbeltas" (al apegarse al principio de segregación de interfaces, por ejemplo). Ese es el punto 2 de mi humilde opinión en la respuesta de @ DavidArno (supongo que no es necesario que repita eso en otra respuesta).Respuestas:
Tiene razón en que esto es una violación del principio de "no probar los elementos internos" y es algo común que la gente pasa por alto.
Hay dos soluciones que puede adoptar para evitar esta violación:
1) Proporcionar una simulación completa de
IPersonRepository
. Su enfoque actualmente descrito es acoplar el simulacro al funcionamiento interno del método bajo prueba solo burlándose de los métodos que llamará. Si proporciona simulacros para todos los métodos deIPersonRepository
, entonces elimina ese acoplamiento. El funcionamiento interno puede cambiar sin afectar el simulacro, lo que hace que la prueba sea menos frágil.Este enfoque tiene la ventaja de mantener el mecanismo DI simple, pero puede crear mucho trabajo si su interfaz define muchos métodos.
2) No inyectar
IPersonRepository
, inyectar elGetSavingsByCustomerId
método o inyectar un valor de ahorro. El problema con la inyección de implementaciones de interfaz completas es que luego está inyectando ("decir, no preguntar") un sistema "preguntar, no decir", mezclando los dos enfoques. Si adopta un enfoque de "DI puro", el método debe recibir (decir) el método exacto para llamar si desea un valor de ahorro, en lugar de recibir un objeto (que luego debe solicitar efectivamente el método para llamar).La ventaja de este enfoque es que evita la necesidad de simulacros (más allá de los métodos de prueba que se inyectan en el método bajo prueba). La desventaja es que puede hacer que las firmas de métodos cambien en respuesta a los requisitos del cambio de método.
Ambos enfoques tienen sus pros y sus contras, así que elija el que mejor se adapte a sus necesidades.
fuente
Mi enfoque es crear versiones 'simuladas' de los repositorios que leen de archivos simples que contienen los datos necesarios.
Esto significa que la prueba individual no 'sabe' sobre la configuración del simulacro, aunque obviamente el proyecto de prueba general hará referencia al simulacro y tendrá los archivos de configuración, etc.
Esto evita la configuración compleja de objetos simulados que requieren los marcos simulados y le permite utilizar los objetos 'simulados' en instancias reales de su aplicación para pruebas de IU y similares.
Debido a que el 'simulacro' está completamente implementado, en lugar de configurarse específicamente para su escenario de prueba, un cambio de implementación, por ejemplo, supongamos que GetSavingsForCustomer ahora también debería eliminar al cliente. No romperá las pruebas (a menos que, por supuesto, realmente rompa las pruebas) solo necesita actualizar su implementación simulada y todas sus pruebas se ejecutarán en su contra sin cambiar su configuración
fuente
Las pruebas unitarias suelen ser pruebas de caja blanca (tiene acceso al código real). Por lo tanto, está bien conocer aspectos internos hasta cierto punto, sin embargo, para los principiantes es más fácil no hacerlo, porque no debe probar el comportamiento interno (como "llamar primero al método, luego a b y luego a").
Inyectar un simulacro que proporciona datos está bien, ya que su clase (unidad) depende de esos datos externos (o proveedor de datos). ¡Sin embargo, debe verificar los resultados (no la forma de llegar a ellos)! por ejemplo, proporciona una instancia de Persona y verifica que se haya enviado un correo electrónico a la dirección de correo electrónico correcta, por ejemplo, al proporcionar un simulacro para el correo electrónico, ese simulacro no hace nada más que almacenar la dirección de correo electrónico del destinatario para su posterior acceso por su prueba -código. (Creo que Martin Fowler los llama Stubs en lugar de Mocks, sin embargo)
fuente