¿Qué hacer cuando las pruebas TDD revelan una nueva funcionalidad necesaria que también necesita pruebas?

13

¿Qué haces cuando escribes una prueba y llegas al punto en que necesitas pasar la prueba y te das cuenta de que necesitas una pieza adicional de funcionalidad que debería separarse en su propia función? Esa nueva función también necesita ser probada, pero el ciclo TDD dice: Hacer que una prueba falle, hacerla pasar y luego refactorizarla. Si estoy en el paso donde estoy tratando de hacer que mi prueba pase, se supone que no debo salir y comenzar otra prueba fallida para probar la nueva funcionalidad que necesito implementar.

Por ejemplo, estoy escribiendo una clase de puntos que tiene una función WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Estaba escribiendo una prueba para CollidesWithLine cuando me di cuenta de que necesitaría una función LineSegment.Intersects ( LineSegment ) . Pero, ¿debería dejar de hacer lo que estoy haciendo en mi ciclo de prueba para crear esta nueva funcionalidad? Eso parece romper el principio de "Rojo, Verde, Refactor".

¿Debo escribir el código que detecta que LineSegments Intersect dentro de la función CollidesWithLine y refactorizarlo después de que esté funcionando? Eso funcionaría en este caso ya que puedo acceder a los datos de LineSegment , pero ¿qué pasa en los casos en que ese tipo de datos es privado?

Joshua Harris
fuente

Respuestas:

14

Simplemente comente su prueba y el código reciente (o póngalo en un escondite) para que de hecho haya retrocedido el reloj hasta el inicio del ciclo. Luego comience con la LineSegment.Intersects(LineSegment)prueba / código / refactor. Cuando termine, descomente su prueba / código anterior (o retírelo del alijo) y continúe con el ciclo.

Javier
fuente
¿Cómo es esto diferente y luego simplemente ignorarlo y volver más tarde?
Joshua Harris el
1
solo pequeños detalles: no hay una prueba adicional de "ignórame" en los informes, y si usas escondites, el código no se puede distinguir del caso "limpio".
Javier
¿Qué es un alijo? ¿Es eso como el control de versiones?
Joshua Harris el
1
algunos VCS lo implementan como una característica (al menos Git y Fossil). Le permite eliminar un cambio pero guardarlo para volver a aplicarlo algún tiempo después. No es difícil hacerlo manualmente, solo guarde un diff y vuelva al último estado. Más tarde, vuelve a aplicar el diff y continúa.
Javier
6

En el ciclo TDD:

En la fase de "hacer que la prueba pase", se supone que debe escribir la implementación más simple que hará que la prueba pase . Para aprobar su prueba, ha decidido crear un nuevo colaborador para manejar la lógica faltante porque tal vez fue demasiado trabajo poner en su clase de puntos para aprobar su prueba. Ahí es donde radica el problema. Supongo que la prueba que estabas tratando de aprobar fue un paso demasiado grande . Entonces, creo que el problema radica en su propia prueba, debe eliminar / comentar esa prueba y descubrir una prueba más simple que le permita dar un pequeño paso sin introducir la parte LineSegment.Intersects (LineSegment). Una vez que tenga esa prueba aprobada, puede refactorizarsu código (aquí aplicará el principio SRP) moviendo esta nueva lógica a un método LineSegment.Intersects (LineSegment). Sus pruebas aún pasarán porque no habrá cambiado ningún comportamiento, sino que simplemente movió algo de código.

En su solución de diseño actual

Pero para mí, usted tiene un problema de diseño más profundo aquí es que está violando el Principio de responsabilidad única . El papel de un Punto es ... ser un punto, eso es todo. No hay inteligencia en ser un punto, es justo y el valor x e y. Los puntos son tipos de valor . Esto es lo mismo para los segmentos, los segmentos son tipos de valores compuestos de dos puntos. Pueden contener un poco de "inteligencia", por ejemplo, para calcular su longitud en función de la posición de sus puntos. Pero eso es todo.

Ahora decidir si un punto y un segmento están colisionando, es una responsabilidad total por sí solo. Y ciertamente es demasiado trabajo para que un punto o segmento lo maneje solo. No puede pertenecer a la clase Punto, porque de lo contrario, los Puntos sabrán acerca de los Segmentos. Y no puede pertenecer a los segmentos porque los segmentos ya tienen la responsabilidad de cuidar los puntos dentro del segmento y tal vez también calcular la longitud del segmento en sí.

Por lo tanto, esta responsabilidad debería ser propia de otra clase como, por ejemplo, un "PointSegmentCollisionDetector" que tendría un método como:

bool AreInCollision (Punto p, Segmento s)

Y eso es algo que probarías por separado de Puntos y Segmentos.

Lo bueno de ese diseño es que ahora podría tener una implementación diferente de su detector de colisión. Por lo tanto, sería fácil, por ejemplo, comparar su motor de juego (supongo que está escribiendo un juego: p) cambiando su método de detección de colisión en tiempo de ejecución. O para hacer algunos controles / experimentos visuales en tiempo de ejecución entre diferentes estrategias de detección de colisiones.

En este momento, al poner esta lógica en su clase de puntos, está bloqueando las cosas y ejerciendo demasiada responsabilidad en la clase de puntos.

Espero que tenga sentido

código de barras
fuente
Tienes razón en que estaba tratando de probar un cambio demasiado grande y creo que tienes razón en separarlo en una clase de colisión, pero eso me hace hacer una pregunta completamente nueva con la que podrías ayudarme: ¿Debería? usa una interfaz cuando los métodos son solo similares? .
Joshua Harris
2

Lo más fácil de hacer en una forma TDD sería extraer una interfaz para LineSegment y cambiar el parámetro de su método para incorporar la interfaz. Entonces podría burlarse del segmento de línea de entrada y codificar / probar el método Intersect de forma independiente.

Dan Lyons
fuente
1
Sé que ese es el método TDD que más escucho, pero un ILineSegment no tiene sentido. Una cosa es interconectar un recurso externo o algo que podría venir en muchas formas, pero no puedo ver una razón por la que alguna vez adjuntaría alguna de las funciones a otra cosa que no sea un segmento de línea.
Joshua Harris el
0

Con jUnit4 puede usar la @Ignoreanotación para las pruebas que desea posponer.

Agregue la anotación a cada método que desee posponer y continúe escribiendo pruebas para la funcionalidad requerida. Vuelva a circular para refactorizar los casos de prueba anteriores más tarde.

bakoyaro
fuente