Estoy trabajando en un proyecto en el que tenemos que implementar y probar un módulo nuevo. Tenía una arquitectura bastante clara en mente, así que rápidamente escribí las clases y métodos principales y luego comenzamos a escribir pruebas unitarias.
Mientras escribíamos las pruebas, tuvimos que hacer bastantes modificaciones al código original, como
- Hacer públicos los métodos privados para probarlos
- Agregar métodos adicionales para acceder a variables privadas
- Agregar métodos adicionales para inyectar objetos simulados que deben usarse cuando el código se ejecuta dentro de una prueba unitaria.
De alguna manera tengo la sensación de que estos son síntomas de que estamos haciendo algo mal, por ejemplo
- el diseño inicial era incorrecto (algunas funcionalidades deberían haber sido públicas desde el principio),
- el código no se diseñó correctamente para interactuar con las pruebas unitarias (quizás debido al hecho de que comenzamos a diseñar las pruebas unitarias cuando ya se habían diseñado bastantes clases),
- estamos implementando pruebas unitarias de forma incorrecta (por ejemplo, las pruebas unitarias solo deberían probar / abordar directamente los métodos públicos de una API, no los privados),
- una mezcla de los tres puntos anteriores, y quizás algunos problemas adicionales en los que no he pensado.
Como tengo algo de experiencia con las pruebas unitarias pero estoy lejos de ser un gurú, me interesaría mucho leer sus pensamientos sobre estos temas.
Además de las preguntas generales anteriores, tengo algunas preguntas técnicas más específicas:
Pregunta 1. ¿Tiene sentido probar directamente un método privado m de una clase A e incluso hacerlo público para probarlo? ¿O debería suponer que m se prueba indirectamente mediante pruebas unitarias que cubren otros métodos públicos que llaman m?
Pregunta 2. Si una instancia de clase A contiene una instancia de clase B (agregación compuesta), ¿tiene sentido burlarse de B para probar A? Mi primera idea fue que no debería burlarme de B porque la instancia B es parte de la instancia A, pero luego comencé a dudar sobre esto. Mi argumento en contra de burlarse de B es el mismo que para 1: B es privado wrt A y solo se usa para su implementación, por lo tanto, burlarse de B parece que estoy exponiendo detalles privados de A como en (1). Pero tal vez estos problemas indiquen una falla de diseño: tal vez no deberíamos usar la agregación compuesta sino una asociación simple de A a B.
Pregunta 3. En el ejemplo anterior, si decidimos burlarnos de B, ¿cómo inyectamos la instancia de B en A? Aquí hay algunas ideas que tuvimos:
- Inyecte la instancia B como argumento para el constructor A en lugar de crear la instancia B en el constructor A.
- Pase una interfaz BFactory como argumento al constructor A y deje que A use la fábrica para crear su instancia privada de B.
- Use un singleton de BFactory que sea privado para A. Use un método estático A :: setBFactory () para establecer el singleton. Cuando A quiere crear la instancia B, usa el singleton de fábrica si está configurado (el escenario de prueba), crea B directamente si el singleton no está configurado (el escenario del código de producción).
Las dos primeras alternativas me parecen más limpias, pero requieren cambiar la firma del constructor A: cambiar una API solo para que sea más comprobable me parece incómodo, ¿es esta una práctica común?
El tercero tiene la ventaja de que no requiere cambiar la firma del constructor (el cambio a la API es menos invasivo), pero requiere llamar al método estático setBFactory () antes de comenzar la prueba, que es propenso a errores de IMO ( la dependencia implícita de un método requiere que las pruebas funcionen correctamente). Entonces no sé cuál deberíamos elegir.
fuente
Respuestas:
Creo que probar los métodos públicos es suficiente la mayor parte del tiempo.
Si tiene una gran complejidad en sus métodos privados, considere colocarlos en otra clase como métodos públicos y úselos como llamadas privadas a esos métodos en su clase original. De esta manera, puede asegurarse de que ambos métodos en sus clases originales y de utilidad funcionen correctamente.
Confiar en gran medida en los métodos privados es algo a considerar sobre las desiciones de diseño.
fuente
A la pregunta 1: Eso depende. Por lo general, comienza con pruebas unitarias para métodos públicos. A veces se encuentra con un método que desea mantener en privado con A, pero cree que tiene sentido probarlo de forma aislada. Si ese es el caso, debe hacer público m o hacer que la clase de prueba sea
TestA
una clase amigaA
. Pero cuidado, agregar una prueba unitaria para probar m hace que sea un poco más difícil cambiar la firma o el comportamiento de m después; si desea mantener ese "detalle de implementación" de A, puede ser mejor no agregar una prueba de unidad directa.Pregunta 2: La agregación compuesta (C ++ incorporado) no funciona bien cuando se trata de simular una instancia. De hecho, dado que la construcción de B ocurre implícitamente en el constructor de A, no tiene la posibilidad de inyectar la dependencia desde afuera. Si ese es un problema, depende de la forma en que desea probar A: si cree que tiene más sentido probar A de forma aislada, con una simulación de B en lugar de B, mejor utilice una asociación simple. Si cree que puede escribir todas las pruebas unitarias necesarias para A sin burlarse de B, entonces un compuesto probablemente estará bien.
Pregunta 3: cambiar una API para hacer que las cosas sean más comprobables es común siempre que no tenga mucho código hasta ahora confiando en esa API. Cuando estás haciendo TDD, no tienes que cambiar tu API después para hacer las cosas más comprobables, comienzas inicialmente con una API diseñada para la comprobabilidad. Si desea cambiar una API más adelante para hacer las cosas más comprobables, puede encontrar problemas, eso es cierto. Así que iría con la primera o segunda de las alternativas que describiste, siempre y cuando puedas cambiar tu API sin problemas, y usar algo como tu tercera alternativa (observación: esto también funciona sin el patrón singleton) solo como último recurso si debo No cambie la API bajo ninguna circunstancia.
Sobre sus preocupaciones de que podría "hacerlo mal": cada gran motor o máquina tiene aperturas de mantenimiento, por lo que en mi humilde opinión, la necesidad de agregar algo así al software no es demasiado sorprendente.
fuente
Debe buscar Inyección de dependencia e Inversión de control. Misko Hevery explica mucho en su blog. DI e IoC es efectivamente un patrón de diseño para hacer código que sea fácil de probar y simular.
Pregunta 1: No, no haga públicos los métodos privados para probarlos. Si el método es suficientemente complejo, puede crear una clase de colaborador que contenga solo ese método e inyectarlo (pasar al constructor) en su otra clase. 1
Pregunta 2: ¿Quién construye A en este caso? Si tiene una clase de fábrica / constructor que construye A, entonces no hay ningún daño en pasar al colaborador B al constructor. Si A está en un paquete / espacio de nombres separado del código que lo usa, incluso puede hacer que el paquete constructor sea privado y hacerlo de modo que la fábrica / generador sea la única clase que pueda construirlo.
Pregunta 3: Respondí esto en la Pregunta 2. Sin embargo, algunas notas adicionales:
1 Esta es la respuesta C # / Java: C ++ puede tener características adicionales que faciliten esto
Como respuesta a sus comentarios:
Lo que quise decir es que su código de producción se cambiaría de (por favor, perdone mi pseudo-C ++):
a:
Entonces su prueba puede ser:
De esta manera, no necesita pasar la fábrica, el código que llama a A no necesita saber cómo construir A, y la prueba puede construir A a medida para permitir que se pruebe el comportamiento.
En su otro comentario, preguntó sobre dependencias anidadas. Entonces, por ejemplo, si sus dependencias fueran:
La primera pregunta es si A usa C y D. Si no lo están, ¿por qué están incluidos en A? Suponiendo que se usan, entonces quizás sea necesario pasar C en su fábrica y hacer que su prueba construya un MockC que devuelva un MockB, lo que le permite probar todas las posibles interacciones.
Si esto comienza a complicarse, puede ser una señal de que su diseño quizás esté demasiado ajustado. Si puede aflojar el acoplamiento y mantener la cohesión alta, entonces este tipo de DI se vuelve más fácil de implementar.
fuente