Nuevo en las pruebas unitarias, ¿cómo escribir excelentes pruebas? [cerrado]

267

Soy bastante nuevo en el mundo de las pruebas unitarias, y esta semana decidí agregar cobertura de prueba para mi aplicación existente.

Esta es una tarea enorme, principalmente debido a la cantidad de clases para evaluar, pero también porque escribir exámenes es algo nuevo para mí.

Ya he escrito pruebas para un montón de clases, pero ahora me pregunto si lo estoy haciendo bien.

Cuando escribo pruebas para un método, tengo la sensación de reescribir por segunda vez lo que ya escribí en el método mismo.
Mis pruebas parecen estar tan ligadas al método (probar todas las rutas de código, esperando que algunos métodos internos se llamen varias veces, con ciertos argumentos), que parece que si alguna vez refactorizo ​​el método, las pruebas fallarán incluso si el El comportamiento final del método no cambió.

Esto es solo un sentimiento, y como dije antes, no tengo experiencia en pruebas. Si algunos probadores más experimentados pudieran darme consejos sobre cómo escribir excelentes pruebas para una aplicación existente, eso sería muy apreciado.

Editar: Me gustaría agradecer a Stack Overflow, tuve excelentes aportes en menos de 15 minutos que respondieron más de las horas de lectura en línea que acabo de hacer.

pixelastic
fuente
1
Este es el mejor libro para pruebas unitarias: manning.com/osherove Explica todas las mejores prácticas, qué hacer y qué no hacer para las pruebas unitarias.
Ervi B
Una cosa que todas estas respuestas dejan de lado es que las pruebas unitarias son como documentación. Ergo, si escribes una función, documentarías su intención, describiendo sus entradas y salidas (y, posiblemente, los efectos secundarios). Una prueba unitaria está destinada a verificar esto, entonces. Y si usted (u otra persona) luego realiza cambios en el código, los documentos deben explicar los límites de los cambios que se pueden realizar, y las pruebas unitarias aseguran que se mantengan los límites.
Thomas Tempelmann el

Respuestas:

187

Mis pruebas parecen estar tan vinculadas al método (probar todas las rutas de código, esperando que algunos métodos internos se llamen varias veces, con ciertos argumentos), que parece que si alguna vez refactorizo ​​el método, las pruebas fallarán incluso si el El comportamiento final del método no cambió.

Creo que lo estás haciendo mal.

Una prueba unitaria debe:

  • prueba un método
  • proporcionar algunos argumentos específicos a ese método
  • probar que el resultado es el esperado

No debe mirar dentro del método para ver lo que está haciendo, por lo que cambiar las partes internas no debería hacer que la prueba falle. No debe probar directamente que se están llamando métodos privados. Si está interesado en averiguar si su código privado se está probando, utilice una herramienta de cobertura de código. Pero no se obsesione con esto: el 100% de cobertura no es un requisito.

Si su método llama a métodos públicos en otras clases, y su interfaz garantiza estas llamadas, entonces puede probar que estas llamadas se realizan utilizando un marco de imitación.

No debe usar el método en sí (ni ninguno de los códigos internos que usa) para generar el resultado esperado dinámicamente. El resultado esperado debe estar codificado en su caso de prueba para que no cambie cuando cambie la implementación. Aquí hay un ejemplo simplificado de lo que debe hacer una prueba unitaria:

testAdd()
{
    int x = 5;
    int y = -2;
    int expectedResult = 3;
    Calculator calculator = new Calculator();
    int actualResult = calculator.Add(x, y);
    Assert.AreEqual(expectedResult, actualResult);
}

Tenga en cuenta que no se verifica cómo se calcula el resultado, solo que el resultado es correcto. Siga agregando más y más casos de prueba simples como los anteriores hasta que haya cubierto tantos escenarios como sea posible. Use su herramienta de cobertura de código para ver si se ha perdido alguna ruta interesante.

Mark Byers
fuente
13
Muchas gracias, su respuesta fue la más completa. Ahora entiendo mejor para qué son realmente los objetos simulados: no necesito hacer valer cada llamada a otros métodos, solo a los relevantes. Tampoco necesito saber CÓMO se hacen las cosas, sino que lo hacen correctamente.
pixelastic
2
Respetuosamente creo que lo estás haciendo mal. Las pruebas unitarias son sobre el flujo de ejecución de código (prueba de caja blanca). La prueba de recuadro negro (lo que está sugiriendo) suele ser la técnica utilizada en las pruebas funcionales (pruebas de sistema e integración).
Wes
1
"Una prueba unitaria debería probar un método" En realidad no estoy de acuerdo. Una prueba unitaria debe probar un concepto lógico. Si bien eso a menudo se representa como un método, ese no es siempre el caso
robertmain
35

Para las pruebas unitarias, encontré que tanto Test Driven (pruebas primero, código segundo) como código primero, prueba segundo, son extremadamente útiles.

En lugar de escribir código, luego escribir prueba. Escribe el código y luego mira lo que PIENSAS que debería estar haciendo el código. Piense en todos los usos previstos y luego escriba una prueba para cada uno. Encuentro que las pruebas de escritura son más rápidas pero más complicadas que la codificación en sí. Las pruebas deben probar la intención. También pensando en las intenciones que terminas encontrando casos de esquina en la fase de escritura de prueba. Y, por supuesto, al escribir pruebas, puede encontrar que uno de los pocos usos causa un error (algo que a menudo encuentro, y estoy muy contento de que este error no haya corrompido los datos y no haya sido verificado).

Sin embargo, las pruebas son casi como codificar dos veces. De hecho, tuve aplicaciones donde había más código de prueba (cantidad) que código de aplicación. Un ejemplo fue una máquina de estado muy compleja. Tenía que asegurarme de que después de agregarle más lógica, todo funcionaba en todos los casos de uso anteriores. Y dado que esos casos eran bastante difíciles de seguir mirando el código, terminé teniendo un conjunto de pruebas tan bueno para esta máquina que estaba seguro de que no se rompería incluso después de hacer cambios, y las pruebas me salvaron el culo varias veces . Y a medida que los usuarios o probadores encontraban errores con los casos de flujo o de esquina no contabilizados, adivinen qué, se agregaron a las pruebas y nunca volvieron a suceder. Esto realmente les dio a los usuarios confianza en mi trabajo además de hacer que todo fuera súper estable. Y cuando tuvo que ser reescrito por razones de rendimiento, adivina qué,

Todos los ejemplos simples function square(number)son geniales y probablemente son malos candidatos para pasar mucho tiempo probando. Los que hacen lógica comercial importante, ahí es donde las pruebas son importantes. Prueba los requisitos. No solo pruebe las tuberías. Si los requisitos cambian, entonces adivine qué, las pruebas también deben hacerlo.

Las pruebas no deberían ser literalmente pruebas de esa función para invocar la barra de funciones 3 veces. Eso está mal. Verifique si el resultado y los efectos secundarios son correctos, no la mecánica interna.

Dmitriy Likhten
fuente
2
Buena respuesta, me dio la confianza de que escribir pruebas después del código aún puede ser útil y posible.
pixelastic
2
Un perfecto ejemplo reciente. Tenía una función muy simple. Pásalo verdadero, hace una cosa, falso hace otra. MUY SIMPLE. Tuve como 4 pruebas de verificación para asegurarse de que la función hace lo que pretende hacer. Cambio un poco el comportamiento. Ejecutar pruebas, prisioneros de guerra un problema. Lo curioso es que al usar la aplicación, el problema no se manifiesta, es solo en un caso complejo que lo hace. El caso de prueba lo encontró y me ahorré horas de dolor de cabeza.
Dmitriy Likhten
"Las pruebas deberían probar la intención". Creo que esto lo resume, que debe pasar por los usos previstos del código y asegurarse de que el código pueda acomodarlos. También señala el alcance de lo que la prueba realmente debería probar y la idea de que, cuando realiza un cambio de código, en el momento no puede considerar en el futuro cómo ese cambio afecta todos los usos prescritos del código: la prueba defiende contra un cambio que no satisface todos los casos de uso previstos.
Greenstick
18

Vale la pena señalar que las pruebas unitarias de adaptación en el código existente son mucho más difíciles que impulsar la creación de ese código con pruebas en primer lugar. Esa es una de las grandes preguntas al tratar con aplicaciones heredadas ... ¿cómo hacer una prueba unitaria? Esto se ha preguntado muchas veces antes (por lo que puede ser cerrado como una pregunta tonta), y la gente generalmente termina aquí:

Mover el código existente al desarrollo impulsado por pruebas

Secundo la recomendación del libro de respuestas aceptadas, pero más allá de eso hay más información vinculada en las respuestas allí.

David
fuente
3
Si escribe pruebas primero o segundo, está bien, pero al escribir pruebas se asegura de que su código sea comprobable para que pueda escribir pruebas. Terminas pensando "cómo puedo probar esto" a menudo que en sí mismo hace que se escriba un mejor código. La adaptación de los casos de prueba es siempre un gran no-no. Muy duro. No es un problema de tiempo, es un problema de cantidad y capacidad de prueba. No puedo acercarme a mi jefe en este momento y decirle que quiero escribir casos de prueba para nuestras más de mil tablas y usos, es demasiado ahora, me llevaría un año, y algunas de las lógicas / decisiones se olvidan. Así que no lo
pospongas
2
Presumiblemente, la respuesta aceptada ha cambiado. Hay una respuesta de Linx que recomienda El arte de las pruebas unitarias por Roy Osherove, manning.com/osherove
thelem
15

No escriba pruebas para obtener una cobertura completa de su código. Escriba pruebas que garanticen sus requisitos. Puede descubrir rutas de código innecesarias. Por el contrario, si son necesarios, están allí para cumplir algún tipo de requisito; búsquelo y pruebe el requisito (no la ruta).

Mantenga sus pruebas pequeñas: una prueba por requisito.

Más tarde, cuando necesite hacer un cambio (o escribir un nuevo código), intente escribir una prueba primero. Solo uno. Entonces habrá dado el primer paso en el desarrollo basado en pruebas.

Jon Reid
fuente
Gracias, tiene sentido tener solo pruebas pequeñas para requisitos pequeños, una a la vez. Lección aprendida.
pixelastic
13

Las pruebas unitarias se refieren al resultado que obtiene de una función / método / aplicación. No importa en absoluto cómo se produce el resultado, solo importa que sea correcto. Por lo tanto, su enfoque de contar llamadas a métodos internos y tal es incorrecto. Lo que tiendo a hacer es sentarme y escribir lo que debería devolver un método dados ciertos valores de entrada o un determinado entorno, luego escribir una prueba que compare el valor real devuelto con lo que se me ocurrió.

Fresskoma
fuente
Gracias ! Tenía la sensación de que lo estaba haciendo mal, pero tener a alguien que realmente me lo diga es mejor.
pixelastic
8

Intente escribir una Prueba de unidad antes de escribir el método que va a probar.

Eso definitivamente te obligará a pensar un poco diferente sobre cómo se están haciendo las cosas. No tendrá idea de cómo va a funcionar el método, solo lo que se supone que debe hacer.

Siempre debe probar los resultados del método, no cómo el método obtiene esos resultados.

Justin Niessner
fuente
Sí, me encantaría poder hacer eso, excepto que los métodos ya están escritos. Solo quiero probarlos. Escribiré pruebas antes que métodos en el futuro, aunque.
pixelastic
2
@pixelastic finge que los métodos no se han escrito?
committedandroider
4

Se supone que las pruebas mejoran la mantenibilidad. Si cambia un método y se rompe un examen, eso puede ser algo bueno. Por otro lado, si observa su método como un cuadro negro, no debería importar lo que haya dentro del método. El hecho es que necesita burlarse de las cosas para algunas pruebas, y en esos casos realmente no puede tratar el método como una caja negra. Lo único que puede hacer es escribir una prueba de integración: carga una instancia completamente instanciada del servicio que se está probando y hace que haga lo que se ejecuta en su aplicación. Entonces puedes tratarlo como una caja negra.

When I'm writing tests for a method, I have the feeling of rewriting a second time what I          
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some    
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the   
method did not change.

Esto se debe a que está escribiendo sus pruebas después de escribir su código. Si lo hiciera al revés (escribió las pruebas primero) no se sentiría de esta manera.

hvgotcodes
fuente
Gracias por el ejemplo de la caja negra, no lo he pensado así. Desearía haber descubierto las pruebas unitarias antes, pero desafortunadamente, ese no es el caso y estoy atascado con una aplicación heredada para agregar pruebas. ¿No hay alguna forma de agregar pruebas a un proyecto existente sin que se sientan rotas?
pixelastic
1
Escribir exámenes después es diferente a escribir exámenes antes, por lo que estás atascado con eso. sin embargo, lo que puede hacer es configurar las pruebas para que fallen primero, luego hacerlas pasar poniendo su clase a prueba en ... haga algo así, poniendo su instancia a prueba después de que la prueba inicialmente falle. Lo mismo ocurre con los simulacros: inicialmente, el simulacro no tiene expectativas y fallará porque el método bajo prueba hará algo con el simulacro y luego aprobará la prueba. No me sorprendería si encuentras muchos errores de esta manera.
hvgotcodes
Además, sea realmente específico con sus expectativas. No afirme solo que la prueba devuelve un objeto, pruebe que el objeto tiene varios valores. Comprueba que cuando se supone que un valor es nulo, lo es. También puede dividirlo un poco haciendo algunas refactorizaciones que quería hacer, después de agregar algunas pruebas.
hvgotcodes