Unidad que prueba una clase que usa DI sin probar en componentes internos

12

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 IPersonRepositoryy un IEmailRepositoryinyectado 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 IPersonRepositoryy 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.GetSavingsByCustomerIddevuelve 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.

Michel
fuente
1
Tan pronto como su clase principal espera un IPersonRepositoryobjeto, 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).
Doc Brown

Respuestas:

7

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.GetSavingsByCustomerIddevuelve x. Pero entonces mi prueba unitaria 'sabe' sobre el funcionamiento interno, porque 'sabe' qué métodos burlarse y cuáles no.

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.

¿Cómo puedo probar una clase que ha inyectado dependencias, sin que la prueba conozca las partes internas?

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 de IPersonRepository, 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 el GetSavingsByCustomerIdmé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.

David Arno
fuente
1
Realmente me gusta la idea número 2. No sé por qué no había pensado en eso antes. He estado creando dependencias en los repositorios IR desde hace bastante tiempo sin preguntarme por qué he creado una dependencia en un repositorio IR completo (que en muchos casos contendrá una gran cantidad de firmas de métodos) mientras solo depende de uno o 2 métodos Entonces, en lugar de inyectar un IRepository, podría inyectar o pasar mejor la (única) firma de método necesaria.
Michel
1

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

Ewan
fuente
2
Esto todavía lleva a la situación de que creo un simulacro con datos para exactamente los métodos con los que está trabajando la clase probada. Así que todavía tengo el problema de que cuando se cambia la implementación, la prueba se romperá.
Michel
1
editado para explicar por qué mi enfoque es mejor, aunque aún no es perfecto para el escenario Creo que te refieres
Ewan
1

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)

Andy
fuente