Clase de hormigón burlón: no recomendado

11

Acabo de leer un extracto del libro "Crecimiento de software orientado a objetos" que explica algunas razones por las cuales no se recomienda burlarse de la clase concreta.

Aquí hay un código de muestra de una prueba unitaria para la clase MusicCentre:

public class MusicCentreTest {
  @Test public void startsCdPlayerAtTimeRequested() {
    final MutableTime scheduledTime = new MutableTime();
    CdPlayer player = new CdPlayer() { 
      @Override 
      public void scheduleToStartAt(Time startTime) {
        scheduledTime.set(startTime);
      }
    }

    MusicCentre centre = new MusicCentre(player);
    centre.startMediaAt(LATER);

    assertEquals(LATER, scheduledTime.get());
  }
}

Y su primera explicación:

El problema con este enfoque es que deja implícita la relación entre los objetos. Espero que ya hayamos aclarado que la intención del desarrollo basado en pruebas con objetos simulados es descubrir relaciones entre objetos. Si subclase, no hay nada en el código de dominio para hacer visible dicha relación, solo métodos en un objeto. Esto hace que sea más difícil ver si el servicio que respalda esta relación podría ser relevante en otro lugar y tendré que volver a hacer el análisis la próxima vez que trabaje con la clase.

No puedo entender exactamente lo que quiere decir cuando dice:

Esto hace que sea más difícil ver si el servicio que respalda esta relación podría ser relevante en otro lugar y tendré que volver a hacer el análisis la próxima vez que trabaje con la clase.

Entiendo que el servicio corresponde al MusicCentremétodo llamado startMediaAt.

¿Qué quiere decir con "en otro lugar"?

El extracto completo está aquí: http://www.mockobjects.com/2007/04/test-smell-mocking-concrete-classes.html

Mik378
fuente
Agregué un comentario en su blog, ya que no pude entender qué quería decir con estas citas.
oligofren
@oligofren Es realmente un gran enigma :) ...
Mik378

Respuestas:

6

El autor de esa publicación está promoviendo el uso de interfaces sobre el uso de clases miembro.

It turns out that my MusicCentre object only uses the starting and stopping methods on the CdPlayer, the rest are used by some other part of the system. I'm over-specifying my MediaCentre by requiring it to talk to a CdPlayer, what it actually needs is a ScheduledDevice.

La relación que le preocupa volver a descubrir más tarde es el hecho de que la clase MediaCentre no necesita todo el objeto CdPlayer. Su afirmación es que al usar una interfaz (presumiblemente limitada a solo iniciar | detener) es más fácil entender cuál es realmente la interacción.

"en otro lugar" simplemente significa que otros objetos pueden tener relaciones igualmente limitadas, y no se requiere el objeto miembro completo; un subconjunto de la funcionalidad envuelta a través de una interfaz debería ser suficiente.

El reclamo comienza a tener más sentido cuando explota toda la funcionalidad potencial:

  • comienzo
  • detener
  • pausa
  • grabar
  • orden de juego al azar
  • pistas de muestra, comienzo de la canción
  • pistas de muestra, muestra aleatoria de canción
  • proporcionar información a los medios
  • ...

Ahora su afirmación de "Solo necesito comenzar y detener" tiene más sentido. El uso del objeto miembro concreto en lugar de una interfaz hace que sea menos claro para los futuros desarrolladores lo que realmente se necesita. Ejecutar pruebas unitarias de MediaCentre en todas las demás funciones dentro de CdPlayer es un desperdicio de esfuerzo de prueba ya que pertenecen al estado "no me importa". Si la Recordfunción no funcionaba en este caso, realmente no nos importa, ya que no es necesaria. Pero un futuro mantenedor no necesariamente sabría eso basado en el código, tal como está escrito.

En última instancia, la premisa del autor es usar solo lo que se necesita y dejar en claro a los futuros mantenedores lo que se requería antes. El objetivo es minimizar el reproceso / reanálisis del módulo de código durante el mantenimiento posterior.


fuente
Gracias por esta gran respuesta. Sin embargo, usted dijo: "Ejecutar pruebas unitarias en todas las demás funciones es una pérdida de esfuerzo de prueba ya que pertenecen al estado" no me importa "". ¿No es más bien: "Crear simulacros para cada una de las otras funciones es una pérdida de esfuerzo de prueba ya que pertenecen al estado" no me importa ""?
Mik378
@ Mik378: sí, eso es exactamente a lo que me refería, simplemente lo expresé de manera diferente. Y actualicé mi respuesta para hacerlo más claro.
Pero encuentro que el término "ejecutar pruebas unitarias" es confuso. Eso significaría que MusicCentre está a punto de probar unitario a su colaborador ... mientras que, de hecho, MOCKS a su colaborador para probar unitario sus servicios PROPIOS. Por cierto, ahora entiendo el significado :)
Mik378
@ Mik378: estamos diciendo lo mismo, y probablemente estoy usando una terminología poco precisa para hacerlo. Disculpas por la confusión.
4

Esto hace que sea más difícil ver si el servicio que respalda esta relación podría ser relevante en otro lugar y tendré que volver a hacer el análisis la próxima vez que trabaje con la clase.

Después de pensarlo mucho, obtengo una posible interpretación de esta cita:

El "servicio" citado corresponde al "hecho de la programación". Esto podría expresarse mediante una interfaz bien nombrada y "centrada en un rol" llamada "ScheduledDevice" o expresada implícitamente por una implementación de un método concreto que no dependa de ninguna interfaz.

En el ejemplo anterior, la programación se expresa por todo el objeto con todas las funciones llamado CDPlayer. Por lo tanto, todavía conduce a una relación implícita entre MusicCentre"hecho de programación".

Entonces, si comenzamos a inyectar clases concretas y burlarnos de ellas en objetos de alto nivel; cuando queremos probar estos, tenemos que analizar cada objeto "concreto" inyectado para ver si presenta una relación específica que TENEMOS QUE BURLAR porque están OCULTOS (implícitos). Por el contrario, la codificación SIEMPRE a través de la interfaz permite al desarrollador averiguar directamente qué tipo de relación está a punto de cumplir el objeto de alto nivel y, por lo tanto, detectar características que deben burlarse para aislar la prueba unitaria.

Mik378
fuente
Creo que lo tienes ahora. Lamentablemente, no recibí una notificación de tu comentario.
Steve Freeman
3

El servicio al que me refería aquí era CDPlayer.scheduleToStartAt (). Eso es lo que llama MediaCentre: el colaborador que necesita para funcionar. MediaCentre es el objeto bajo prueba.

La idea es que si hago explícito de qué depende MediaCentre, no una clase de implementación, puedo darle un nombre a ese rol de dependencia y hablar sobre ello. Todo lo que MediaCentre necesita saber es que habla con ScheduledDevices. A medida que el resto del sistema cambia, no necesitaré alterar el MediaCentre a menos que cambien sus características.

¿Eso ayuda?

Steve Freeman
fuente
(autor de este gran artículo :)) lo que quería interpretar era esta oración: "Esto hace que sea más difícil ver si el servicio que respalda esta relación podría ser relevante en otro lugar y tendré que hacer el análisis nuevamente la próxima vez que trabaje con la clase ". ¿Qué tipo de análisis? ¿El hecho de detectar el método de qué objeto se supone que implementa la relación ya que este está claramente oculto?
Mik378