¿Debo probar mis subclases o mi clase abstracta de padres?

12

Tengo una implementación esquelética, como en el ítem 18 de Effective Java (discusión extendida aquí ). Es una clase abstracta que proporciona 2 métodos públicos, el método A () y el método B () que llaman a los métodos de las subclases para "llenar los vacíos" que no puedo definir de manera abstracta.

Lo desarrollé primero creando una clase concreta y escribiendo pruebas unitarias para ello. Cuando llegó la segunda clase, pude extraer un comportamiento común, implementar los "vacíos faltantes" en la segunda clase y estaba listo (por supuesto, las pruebas unitarias se crearon para la segunda subclase).

Pasó el tiempo y ahora tengo 4 subclases, cada una implementando 3 métodos protegidos cortos que son muy específicos para su implementación concreta, mientras que la implementación esquelética hace todo el trabajo genérico.

Mi problema es que cuando creo una nueva implementación, escribo las pruebas nuevamente:

  • ¿La subclase llama a un método requerido dado?
  • ¿Un método dado tiene una anotación dada?
  • ¿Obtengo los resultados esperados del método A?
  • ¿Obtengo los resultados esperados del método B?

Si bien ver ventajas con este enfoque:

  • Documenta los requisitos de la subclase mediante pruebas.
  • Puede fallar rápidamente en caso de una refactorización problemática
  • Pruebe la subclase en su conjunto y a través de su API pública ("¿funciona el método A ()?" Y no "¿funciona este método protegido?")

El problema que tengo es que las pruebas para una nueva subclase son básicamente obvias: - Las pruebas son todas iguales en todas las subclases, solo cambian el tipo de retorno de los métodos (la implementación del esqueleto es genérica) y las propiedades I verifique la parte de afirmación de la prueba. - Por lo general, las subclases no tienen pruebas específicas para ellas.

Me gusta la forma en que las pruebas se centran en el resultado de la subclase en sí y lo protege de la refactorización, pero el hecho de que la implementación de la prueba se convirtiera simplemente en "trabajo manual" me hace pensar que estoy haciendo algo mal.

¿Es este problema común cuando se prueba la jerarquía de clases? ¿Cómo puedo evitar eso?

ps: pensé en probar la clase de esqueleto, pero también parece extraño crear una simulación de una clase abstracta para poder probarla. Y creo que cualquier refactorización en la clase abstracta que cambie el comportamiento que no se espera en la subclase no se notará tan rápido. Mis agallas me dicen que probar la subclase "en su conjunto" es el enfoque preferido aquí, pero no dude en decirme que estoy equivocado :)

ps2: busqué en Google y encontré muchas preguntas sobre el tema. Uno de ellos es el que tiene una gran respuesta de Nigel Thorne, mi caso sería su "número 1". Eso sería genial, pero no puedo refactorizar en este punto, por lo que tendría que vivir con este problema. Si pudiera refactorizar, tendría cada subclase como estrategia. Eso estaría bien, pero aún probaría la "clase principal" y las estrategias, pero no notaría ninguna refactorización que rompa la integración entre la clase principal y las estrategias.

ps3: He encontrado algunas respuestas que dicen que "es aceptable" probar la clase abstracta. Estoy de acuerdo en que esto es aceptable, pero me gustaría saber cuál es el enfoque preferido en este caso (todavía estoy comenzando con las pruebas unitarias)

JSBach
fuente
2
Escriba una clase de prueba abstracta y haga que sus pruebas concretas hereden de ella, reflejando la estructura del código comercial. Se supone que las pruebas prueban el comportamiento y no la implementación de una unidad, pero eso no significa que no pueda usar su conocimiento interno sobre el sistema para lograr ese objetivo de manera más eficiente.
Kilian Foth
Leí en alguna parte que uno no debería usar la herencia en las pruebas, estoy buscando la fuente para leerla detenidamente
JSBach
44
@KilianFoth, ¿estás seguro de este consejo? Esto suena como una receta para pruebas unitarias frágiles, altamente sensible a los cambios de diseño y, por lo tanto, no muy fácil de mantener.
Konrad Morawski

Respuestas:

5

No puedo ver cómo escribirías pruebas para una clase base abstracta ; no puede crear una instancia, por lo que no puede tener una instancia para probar. Puede crear una subclase concreta "ficticia" solo con el propósito de probarla, sin saber cuánto uso sería. YMMV.

Cada vez que crea una nueva implementación (clase), debe escribir pruebas que ejerciten esa nueva implementación. Dado que partes de la funcionalidad (sus "brechas") se implementan dentro de cada subclase, esto es absolutamente necesario; El resultado de cada uno de los métodos heredados puede (y probablemente debería ) ser diferente para cada nueva implementación.

Phill W.
fuente
Sí, son diferentes (los tipos y el contenido son diferentes). Técnicamente hablando, podría burlarme de esta clase o tener una implementación simple solo para fines de prueba con implementaciones vacías. No me gusta este enfoque. Pero el hecho de que no "necesito" pensar en pasar las pruebas en las nuevas implementaciones me hace sentir extraño y también me hace preguntarme qué tan seguro estoy en esta prueba (por supuesto, si cambio la clase base a propósito, el las pruebas fallan, lo que es una prueba de que puedo confiar en ellas)
JSBach
4

Por lo general, crearía una clase base para aplicar una interfaz. Desea probar esa interfaz, no los detalles a continuación. Por lo tanto, debe crear pruebas que creen instancias de cada clase individualmente, pero solo a través de los métodos de la clase base.

Las ventajas de este método es que debido a que probó la interfaz, ahora puede refactorizar debajo de la interfaz. Puede encontrar que el código en una clase secundaria debe estar en el padre, o viceversa. Ahora sus pruebas demostrarán que no rompió el comportamiento cuando movió ese código.

En general, debe probar el código de cómo se debe usar. Eso significa evitar probar métodos privados, y a menudo significa que su unidad bajo prueba puede ser un poco más grande de lo que pensaba originalmente, tal vez un grupo de clases en lugar de una sola clase. Su objetivo debe ser aislar los cambios para que rara vez tenga que cambiar una prueba cuando refactorice en un ámbito determinado.

Michael K
fuente