Cómo corregir un error en la prueba, después de escribir la implementación

21

¿Cuál es el mejor curso de acción en TDD si, después de implementar la lógica correctamente, la prueba todavía falla (porque hay un error en la prueba)?

Por ejemplo, suponga que desea desarrollar la siguiente función:

int add(int a, int b) {
    return a + b;
}

Supongamos que lo desarrollamos en los siguientes pasos:

  1. Prueba de escritura (aún no funciona):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Resultados en el error de compilación.

  2. Escriba una implementación de función ficticia:

    int add(int a, int b) {
        return 5;
    }
    

    Resultado: test1pases.

  3. Agregue otro caso de prueba:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Resultado: test2falla, test1aún pasa.

  4. Escribir implementación real:

    int add(int a, int b) {
        return a + b;
    }
    

    Resultado: test1todavía pasa, test2todavía falla (desde 11 != 12).

En este caso particular: ¿sería mejor:

  1. correcto test2, y ver que ahora pasa, o
  2. elimine la nueva parte de la implementación (es decir, regrese al paso 2 anterior), corríjala test2y deje que falle, y luego reintroduzca la implementación correcta (paso 4 anterior).

¿O hay alguna otra forma más inteligente?

Si bien entiendo que el problema del ejemplo es bastante trivial, estoy interesado en qué hacer en el caso genérico, que podría ser más complejo que la suma de dos números.

EDITAR (en respuesta a la respuesta de @Thomas Junk):

El foco de esta pregunta es lo que TDD sugiere en tal caso, no lo que es "la mejor práctica universal" para lograr un buen código o pruebas (que podrían ser diferentes a la forma TDD).

Attilio
fuente
3
Refactorizar contra la barra roja es un concepto relevante.
RubberDuck
55
Claramente, debe estar haciendo TDD en su TDD.
Blrfl
17
Si alguien me pregunta por qué soy escéptico de TDD, lo señalaré a esta pregunta. Este es Kafkaesque.
Traubenfuchs
@Blrfl eso es lo que nos dice Xibit »Puse el TDD en TDD para que pudieras TDD mientras TDDing«: D
Thomas Junk
3
@Traubenfuchs Admito que la pregunta parece tonta a primera vista y no soy un defensor de "hacer TDD todo el tiempo", pero creo que es un gran beneficio ver que una prueba falla, luego escribir el código que hace que la prueba pase (de eso se trata esta pregunta, después de todo).
Vincent Savard

Respuestas:

31

Lo absolutamente crítico es que vea que la prueba pasa y falla.

Ya sea que elimine el código para que la prueba falle, luego vuelva a escribir el código o lo escabulle al portapapeles solo para pegarlo más tarde, no importa. TDD nunca dijo que tenías que volver a escribir nada. Quiere saber que la prueba pasa solo cuando debe pasar y falla solo cuando debe fallar.

Ver la prueba pasar y fallar es cómo prueba la prueba. Nunca confíes en una prueba que nunca has visto hacer ambas cosas.


Refactoring Against The Red Bar nos da pasos formales para refactorizar una prueba de trabajo:

  • Ejecute la prueba
    • Tenga en cuenta la barra verde
    • Romper el código que se está probando
  • Ejecute la prueba
    • Tenga en cuenta la barra roja
    • Refactorizar la prueba
  • Ejecute la prueba
    • Tenga en cuenta la barra roja
    • Descomprima el código que se está probando
  • Ejecute la prueba
    • Tenga en cuenta la barra verde

Sin embargo, no estamos refactorizando una prueba de trabajo. Tenemos que transformar una prueba de errores. Una preocupación es el código que se introdujo mientras solo esta prueba lo cubría. Dicho código debe revertirse y reintroducirse una vez que se repara la prueba.

Si ese no es el caso, y la cobertura del código no es una preocupación debido a otras pruebas que cubren el código, puede transformar la prueba e introducirla como una prueba verde.

Aquí, el código también se revierte, pero lo suficiente como para hacer que la prueba falle. Si eso no es suficiente para cubrir todo el código introducido mientras solo está cubierto por la prueba de errores, necesitamos una mayor reversión del código y más pruebas.

Introducir una prueba verde

  • Ejecute la prueba
    • Tenga en cuenta la barra verde
    • Romper el código que se está probando
  • Ejecute la prueba
    • Tenga en cuenta la barra roja
    • Descomprima el código que se está probando
  • Ejecute la prueba
    • Tenga en cuenta la barra verde

Romper el código puede ser comentar el código o moverlo a otro lugar solo para pegarlo más tarde. Esto nos muestra el alcance del código que cubre la prueba.

Para estas dos últimas carreras, estás de vuelta en el ciclo rojo verde normal. Simplemente está pegando en lugar de escribir para descifrar el código y hacer que la prueba pase. Por lo tanto, asegúrese de pegar solo lo suficiente para pasar la prueba.

El patrón general aquí es ver cómo cambia el color de la prueba de la manera que esperamos. Tenga en cuenta que esto crea una situación en la que brevemente tiene una prueba verde no confiable. Tenga cuidado de que lo interrumpan y olvide dónde se encuentra en estos pasos.

Mi agradecimiento a RubberDuck por el enlace Abrazando la barra roja .

naranja confitada
fuente
2
Me gusta más esta respuesta: es importante ver que la prueba falla con un código incorrecto, por lo que eliminaría / comentaría el código, corregiría las pruebas y vería que fallaban, volvería a colocar el código (tal vez introduzca un error deliberado para poner las pruebas a la prueba) y corrija el código para que funcione. Es muy XP eliminarlo y reescribirlo por completo, pero a veces solo tienes que ser pragmático. ;)
GolezTrol
@GolezTrol Creo que mi respuesta dice lo mismo, así que agradecería cualquier comentario que haya tenido sobre si eso no estaba claro.
jonrsharpe
@jonrsharpe Tu respuesta también es buena, y la voté antes incluso de leer esta. Pero cuando eres muy estricto al revertir el código, CandiedOrange sugiere un enfoque más pragmático que me atrae más.
GolezTrol
@GolezTrol No dije cómo revertir el código; comentarlo, cortarlo y pegarlo, guardarlo, usar el historial de su IDE; Realmente no importa. Lo crucial es por qué lo hace: para que pueda verificar que está obteniendo la falla correcta . He editado, espero aclarar.
jonrsharpe
10

¿Cuál es el objetivo general que quieres lograr?

  • Haciendo buenas pruebas?

  • ¿Hacer la implementación correcta ?

  • ¿Hacer TTD religiosamente correcto ?

  • ¿Ninguna de las anteriores?

Quizás pienses demasiado en tu relación con las pruebas y las pruebas.

Las pruebas no garantizan la corrección de una implementación. Tener todas las pruebas aprobadas no dice nada acerca de si su software hace lo que debería; no hace declaraciones esenciales sobre su software.

Tomando tu ejemplo:

La implementación "correcta" de la adición sería el código equivalente a a+b. Y mientras su código haga eso, diría que el algoritmo es correcto en lo que hace y está implementado correctamente .

int add(int a, int b) {
    return a + b;
}

A primera vista , ambos estaríamos de acuerdo en que esta es la implementación de una adición.

Pero lo que estamos haciendo realmente no es decir, que este código es la implementación addition, solo se comporta en cierto grado como uno: piense en el desbordamiento de enteros .

El desbordamiento de enteros ocurre en el código, pero no en el concepto de addition. Entonces: su código se comporta en cierta medida como el concepto de addition, pero no lo es addition.

Este punto de vista más bien filosófico tiene varias consecuencias.

Y una es que, podría decirse, las pruebas no son más que suposiciones del comportamiento esperado de su código. Al probar su código, podría (quizás) nunca asegurarse de que su implementación sea correcta , lo mejor que podría decir es que sus expectativas sobre los resultados que entrega su código se cumplieron o no; ya sea que su código esté equivocado, que su prueba esté equivocada o que ambos estén equivocados.

Las pruebas útiles lo ayudan a fijar sus expectativas sobre lo que debe hacer el código: siempre que no cambie mis expectativas y mientras el código modificado me dé el resultado que estoy esperando, podría estar seguro de que las suposiciones que hice sobre Los resultados parecen funcionar.

Eso no ayuda, cuando hiciste las suposiciones equivocadas; ¡pero hey! al menos previene la esquizofrenia: espera resultados diferentes cuando no debería haber ninguno.


tl; dr

¿Cuál es el mejor curso de acción en TDD si, después de implementar la lógica correctamente, la prueba todavía falla (porque hay un error en la prueba)?

Sus pruebas son suposiciones sobre el comportamiento del código. Si tiene buenas razones para pensar que su implementación es correcta, repare la prueba y vea si esa suposición se cumple.

Thomas Junk
fuente
1
Creo que la pregunta sobre los objetivos generales es bastante importante, gracias por mencionarlo. Para mí, el prio más alto es el siguiente: 1. implementación correcta 2. pruebas "agradables" (o, mejor dicho, pruebas "útiles" / "bien diseñadas"). Veo TDD como una posible herramienta para lograr esos dos objetivos. Entonces, aunque no quiero necesariamente seguir religiosamente a TDD, en el contexto de esta pregunta, estoy principalmente interesado en la perspectiva de TDD. Editaré la pregunta para aclarar esto.
Attilio
Entonces, ¿escribiría una prueba que pruebe el desbordamiento y pase cuando suceda o lo haría fallar cuando suceda porque el algoritmo es suma y el desbordamiento produce la respuesta incorrecta?
Jerry Jeremiah
1
@JerryJeremiah Mi punto es: lo que deberían cubrir sus pruebas depende de su caso de uso. Para un caso de uso en el que se suman varios dígitos individuales, el algoritmo es lo suficientemente bueno . Si sabe que es muy probable que sume "números grandes", esta datatypees claramente la elección incorrecta. Una prueba revelaría que: su expectativa sería »funciona para grandes números« y en varios casos no se cumple. Entonces la pregunta sería cómo lidiar con esos casos. ¿Son casos de esquina? Cuando sí, ¿cómo lidiar con ellos? Quizás algunas cláusulas de quard ayudan a prevenir un mayor desorden. La respuesta está vinculada al contexto.
Thomas Junk
7

Debe saber que la prueba fallará si la implementación es incorrecta, lo cual no es lo mismo que aprobar si la implementación es correcta. Por lo tanto, debe volver a colocar el código en un estado en el que espera que falle antes de corregir la prueba, y asegurarse de que falle por la razón que esperaba (es decir 5 != 12), en lugar de otra cosa que no predijo.

jonrsharpe
fuente
¿Cómo podemos verificar que la prueba está fallando por la razón que esperamos?
Basilevs
2
@Basilevs usted: 1. formula una hipótesis sobre cuál debería ser la razón del fracaso; 2. ejecutar la prueba; y 3. leer el mensaje de falla resultante y comparar. A veces, esto también sugiere formas en que podría reescribir la prueba para obtener un error más significativo (por ejemplo, assertTrue(5 == add(2, 3))proporciona un resultado menos útil que assertEqual(5, add(2, 3))aunque ambos estén probando lo mismo).
jonrsharpe
Todavía no está claro cómo aplicar este principio aquí. Tengo una hipótesis: la prueba devuelve un valor constante, ¿cómo ejecutar la misma prueba nuevamente garantizaría que tengo razón? Obviamente para probar eso, necesito OTRA prueba. Sugiero agregar un ejemplo explícito para responder.
Basilevs
1
@Basilevs qué? Su hipótesis aquí en el paso 3 sería "la prueba falla porque 5 no es igual a 12" . La ejecución de la prueba le mostrará si la prueba falla por esa razón, en cuyo caso proceda, o por alguna otra razón, en cuyo caso se da cuenta de por qué. Quizás sea un problema de idioma, pero no me queda claro lo que sugieres.
jonrsharpe
5

En este caso particular, si cambia el 12 a un 11, y la prueba ahora pasa, creo que ha hecho un buen trabajo probando la prueba y la implementación, por lo que no hay mucha necesidad de pasar por aros adicionales.

Sin embargo, el mismo problema puede surgir en situaciones más complejas, como cuando tiene un error en su código de configuración. En ese caso, después de arreglar su prueba, probablemente debería intentar mutar su implementación de tal manera que esa prueba en particular falle y luego revertir la mutación. Si revertir la implementación es la forma más fácil de hacerlo, está bien. En su ejemplo, puede mutar a + ba a + ao a * b.

Alternativamente, si puede mutar ligeramente la afirmación y ver que la prueba falla, eso puede ser bastante efectivo para probar la prueba.

Vaughn Cato
fuente
0

Yo diría que este es un caso para su sistema de control de versiones favorito:

  1. Organice la corrección de la prueba, manteniendo los cambios de código en su directorio de trabajo.
    Comprometerse con un mensaje correspondiente Fixed test ... to expect correct output.

    Con git, esto podría requerir el uso de git add -psi la prueba y la implementación están en el mismo archivo, de lo contrario, obviamente, solo puede organizar los dos archivos por separado.

  2. Comprometer el código de implementación.

  3. Retroceda en el tiempo para probar la confirmación realizada en el paso 1, asegurándose de que la prueba realmente falle .

Verá, de esa manera no confía en su destreza de edición para mover su código de implementación fuera del camino mientras prueba su prueba fallida. Emplea su VCS para guardar su trabajo y para asegurarse de que el historial registrado de VCS incluya correctamente tanto la prueba reprobatoria como la aprobada.

cmaster - restablecer monica
fuente