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.