En TDD, si escribo un caso de prueba que pasa sin modificar el código de producción, ¿qué significa eso?

17

Estas son las reglas de Robert C. Martin para TDD :

  • No está permitido escribir ningún código de producción a menos que sea para aprobar una prueba de unidad que falla.
  • No se le permite escribir más de una prueba unitaria de la que es suficiente para fallar; y las fallas de compilación son fallas.
  • No está permitido escribir más código de producción del que sea suficiente para pasar la prueba de una unidad que falla.

Cuando escribo una prueba que parece que vale la pena pero que pasa sin cambiar el código de producción:

  1. ¿Eso significa que hice algo mal?
  2. ¿Debo evitar escribir tales pruebas en el futuro si me pueden ayudar?
  3. ¿Debo dejar esa prueba allí o eliminarla?

Nota: Estaba tratando de hacer esta pregunta aquí: ¿Puedo comenzar con una prueba de unidad aprobada? Pero no pude articular la pregunta lo suficientemente bien hasta ahora.

Daniel Kaplan
fuente
El "Juego de bolos Kata" vinculado en el artículo que cita en realidad tiene una prueba de aprobación inmediata como paso final.
jscs

Respuestas:

21

Dice que no puede escribir código de producción a menos que sea para aprobar una prueba de unidad que falla y no que no pueda escribir una prueba que pase desde el principio. La intención de la regla es decir "Si necesita editar el código de producción, asegúrese de escribir o cambiar una prueba primero".

A veces escribimos pruebas para probar una teoría. La prueba pasa y eso refuta nuestra teoría. Entonces no eliminamos la prueba. Sin embargo, podríamos (sabiendo que tenemos el respaldo del control de origen) romper el código de producción, para asegurarnos de que entendemos por qué pasó cuando no lo esperábamos.

Si resulta ser una prueba válida y correcta, y no está duplicando una prueba existente, déjelo allí.

pdr
fuente
Mejorar la cobertura de prueba del código existente es otra razón perfectamente válida para escribir (con suerte) una prueba aprobada.
Jack
13

Significa que:

  1. Usted escribió el código de producción que cumple con la función que desea sin escribir primero la prueba (una violación de "TDD religioso"), o
  2. La característica que necesita ya está cumplida por el código de producción, y solo está escribiendo otra prueba unitaria para cubrir esa característica.

La última situación es más común de lo que piensas. Como un ejemplo completamente engañoso y trivial (pero aún ilustrativo), digamos que usted escribió la siguiente prueba de unidad (pseudocódigo, porque soy vago):

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

Porque todo lo que realmente necesitas es el resultado de 2 y 3 sumados.

Su método de implementación sería:

public int add(int x, int y)
{
    return x + y;
}

Pero digamos que ahora necesito agregar 4 y 6 juntos:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

No necesito reescribir mi método, porque ya cubre el segundo caso.

Ahora digamos que descubrí que mi función Agregar realmente necesita devolver un número que tenga un límite máximo, digamos 100. Puedo escribir un nuevo método que pruebe esto:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

Y esta prueba ahora fallará. Ahora debo reescribir mi función

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

para hacerlo pasar.

El sentido común dicta que si

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

pasa, no hace que su método falle deliberadamente solo para que pueda tener una prueba fallida y pueda escribir un nuevo código para hacer que pase la prueba.

Robert Harvey
fuente
55
Si seguiste completamente los ejemplos de Martin (y él no necesariamente sugiere que lo hagas), para hacer un add(2,3)pase, literalmente regresarías 5. Codificado. Luego , escribiría la prueba para lo add(4,6)cual lo obligaría a escribir el código de producción que lo hace pasar sin romperse add(2,3)al mismo tiempo. Se podría acabar con el return x + y, pero no sería empezar con él. En teoria. Naturalmente, a Martin (o tal vez fue otra persona, no recuerdo) le gusta proporcionar tales ejemplos para la educación, pero no espera que realmente escriba un código tan trivial de esa manera.
Anthony Pegram
1
@tieTYT, por lo general, si recuerdo correctamente el libro de Martin, el segundo caso de prueba generalmente sería suficiente para que escribas la solución general para un método simple (y, en realidad, simplemente harías que funcione primera vez). No hay necesidad de un tercero.
Anthony Pegram
2
@tieTYT, entonces seguirías escribiendo pruebas hasta que lo hicieras. :)
Anthony Pegram
44
Hay una tercera posibilidad, y va en contra de su ejemplo: escribió una prueba duplicada. Si sigue TDD "religiosamente", entonces una nueva prueba que pasa es siempre una bandera roja. Después de DRY , nunca debe escribir dos pruebas que prueben esencialmente lo mismo.
congusbongus
1
"Si seguiste completamente los ejemplos de Martin (y él no necesariamente sugiere que lo hagas), para hacer pasar add (2,3), literalmente devolverías 5. Codificado". - Esta es la parte estricta de TDD que siempre me ha molestado, la idea de que escribes un código que sabes que está mal en la expectativa de una futura prueba que lo demuestre. ¿Qué pasa si esa prueba futura nunca se escribe, por alguna razón, y los colegas suponen que "todas las pruebas verdes" implican "todo el código correcto"?
Julia Hayward
2

Su prueba pasa pero no está equivocado. Creo que sucedió porque el código de producción no es TDD desde el principio.

Supongamos que TDD canónico (?). No hay código de producción, pero hay algunos casos de prueba (que por supuesto siempre fallan). Agregamos código de producción para pasar. Luego, deténgase aquí para agregar más casos de prueba fallida. Nuevamente agregue el código de producción para pasar.

En otras palabras, su prueba podría ser un tipo de prueba de funcionalidad, no una simple prueba de unidad TDD. Esos son siempre activos valiosos para la calidad del producto.

Personalmente no me gustan tales reglas totalitarias e inhumanas; (

9dan
fuente
2

En realidad, el mismo problema surgió en un dojo anoche.

Hice una investigación rápida al respecto. Esto es lo que se me ocurrió:

Básicamente no está prohibido explícitamente por las reglas TDD. Quizás se necesiten algunas pruebas adicionales para demostrar que una función funciona correctamente para una entrada generalizada. En este caso, la práctica de TDD se deja de lado por un momento. Tenga en cuenta que dejar la práctica de TDD en breve no es necesariamente romper las reglas de TDD siempre y cuando no se agregue ningún código de producción mientras tanto.

Se pueden escribir pruebas adicionales siempre que no sean redundantes. Una buena práctica sería hacer pruebas de particionamiento de clase de equivalencia. Eso significa que se prueban los casos límite y al menos un caso interno para cada clase de equivalencia.

Sin embargo, un problema que podría ocurrir con este enfoque es que si las pruebas pasan desde el principio, no se puede asegurar que no haya falsos positivos. Esto significa que podrían pasar pruebas porque las pruebas no se implementan correctamente y no porque el código de producción funciona correctamente. Para evitar esto, el código de producción debe cambiarse ligeramente para romper la prueba. Si esto hace que la prueba falle, lo más probable es que se implemente correctamente y el código de producción se puede volver a cambiar para que la prueba pase nuevamente.

Si solo desea practicar TDD estricto, es posible que no escriba ninguna prueba adicional que pase desde el principio. Por otro lado, en un entorno de desarrollo empresarial, uno debería abandonar la práctica de TDD si las pruebas adicionales parecen útiles.

Leifbattermann
fuente
0

Una prueba que pasa sin modificar el código de producción no es inherentemente mala, y a menudo es necesaria para describir un requisito adicional o un caso límite. Mientras su prueba "parezca que valga la pena", como usted dice que la suya lo hace, consérvela.

Donde te metes en problemas es cuando escribes una prueba que ya pasó como un reemplazo para entender realmente el espacio del problema.

Podemos imaginarnos en dos extremos: un programador que escribe una gran cantidad de pruebas "por si acaso" uno atrapa un error; y un segundo programador que analiza cuidadosamente el espacio del problema antes de escribir un número mínimo de pruebas. Digamos que ambos están tratando de implementar una función de valor absoluto.

El primer programador escribe:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

El segundo programador escribe:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

La implementación del primer programador podría resultar en:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

La implementación del segundo programador podría resultar en:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

Todas las pruebas pasan, pero el primer programador no solo ha escrito varias pruebas redundantes (ralentizando innecesariamente su ciclo de desarrollo), sino que tampoco ha podido probar un caso límite ( abs(0)).

Si se encuentra escribiendo pruebas que pasan sin modificar el código de producción, pregúntese si sus pruebas realmente están agregando valor o si necesita pasar más tiempo entendiendo el espacio del problema.

pensamiento
fuente
Bueno, el segundo programador fue claramente descuidado con las pruebas también, porque su compañero de trabajo redefinió abs(n) = n*ny aprobó.
Eiko
@Eiko Tienes toda la razón. Escribir muy pocas pruebas puede morderte igual de mal. El segundo programador fue demasiado tacaño al no probar al menos abs(-2). Como con todo, la moderación es la clave.
thinkterry