¿Qué tan profundas son sus pruebas unitarias?

88

Lo que he descubierto sobre TDD es que se necesita tiempo para configurar las pruebas y, siendo naturalmente vago, siempre quiero escribir la menor cantidad de código posible. Lo primero que parece que hago es probar que mi constructor ha establecido todas las propiedades, pero ¿es esto excesivo?

Mi pregunta es ¿en qué nivel de granularidad escribe sus pruebas unitarias?

..y hay un caso de probar demasiado?

Johnno Nolan
fuente

Respuestas:

221

Me pagan por el código que funciona, no por las pruebas, por lo que mi filosofía es probar lo menos posible para alcanzar un nivel de confianza determinado (sospecho que este nivel de confianza es alto en comparación con los estándares de la industria, pero eso podría ser simplemente arrogancia). . Si normalmente no cometo algún tipo de error (como establecer las variables incorrectas en un constructor), no lo pruebo. Tiendo a dar sentido a los errores de prueba, así que soy muy cuidadoso cuando tengo lógica con condicionales complicados. Al programar en un equipo, modifico mi estrategia para probar cuidadosamente el código que, colectivamente, tendemos a equivocarnos.

Diferentes personas tendrán diferentes estrategias de prueba basadas en esta filosofía, pero eso me parece razonable dado el estado inmaduro de comprensión de cómo las pruebas pueden encajar mejor en el ciclo interno de la codificación. Es probable que dentro de diez o veinte años tengamos una teoría más universal sobre qué pruebas escribir, qué pruebas no escribir y cómo diferenciar. Mientras tanto, la experimentación parece estar en orden.

Kent Beck
fuente
40
¡El mundo no cree que Kent Beck diría esto! ¡Hay legiones de desarrolladores que persiguen diligentemente una cobertura del 100% porque creen que es lo que haría Kent Beck! Les he dicho a muchos que usted dijo, en su libro de XP, que no siempre se adhiere religiosamente a Test First. Pero también me sorprende.
Charlie Flowers
6
En realidad, no estoy de acuerdo, porque el código que produce un desarrollador no es el suyo, y en el siguiente sprint, alguien más lo cambiará y cometerá errores que usted "sabe que no". También TDD, piensa primero en las pruebas. Entonces, si realiza TDD asumiendo que debe probar parte del código, lo está haciendo mal
Ricardo Rodrigues
2
No me interesa la cobertura. Estoy muy interesado en la frecuencia con la que el Sr. Beck confirma código que no fue escrito en respuesta a una prueba fallida.
sheldonh
1
@RicardoRodrigues, no puedes escribir pruebas para cubrir el código que otras personas escribirán más tarde. Esa es su responsabilidad.
Kief
2
Eso no es lo que escribí, lea con atención; Escribí que si escribes pruebas para cubrir solo una parte de tu propio código, dejando partes descubiertas en las que "sabías que no cometes errores" y esas partes se cambian y no tienen las pruebas adecuadas, tienes un problema allí mismo. y eso no es TDD en absoluto.
Ricardo Rodrigues
20

Escriba pruebas unitarias para las cosas que espera romper y para casos extremos. Después de eso, se deben agregar casos de prueba a medida que llegan los informes de errores, antes de escribir la solución para el error. El desarrollador puede estar seguro de que:

  1. El error está arreglado;
  2. El error no volverá a aparecer.

Según el comentario adjunto, creo que este enfoque para escribir pruebas unitarias podría causar problemas si, con el tiempo, se descubren muchos errores en una clase determinada. Probablemente aquí es donde la discreción es útil: agregar pruebas unitarias solo para errores que es probable que vuelvan a ocurrir, o donde su reaparición causaría problemas graves. Descubrí que una medida de las pruebas de integración en las pruebas unitarias puede ser útil en estos escenarios: probar el código de las rutas de código más arriba puede cubrir las rutas de código más abajo.

Dominic Rodger
fuente
Con la cantidad de errores que escribo, esto puede convertirse en un patrón anti. Con cientos de pruebas en el código donde las cosas se han roto, esto puede significar que sus pruebas se vuelven ilegibles y cuando llega el momento de reescribir esas pruebas puede convertirse en una sobrecarga.
Johnno Nolan
@JohnNolan: ¿Es la legibilidad de las pruebas tan importante? En mi humilde opinión, no lo es, al menos para estas pruebas de regresión específicas de errores. Si está reescribiendo pruebas con frecuencia, es posible que esté probando a un nivel demasiado bajo; idealmente, sus interfaces deberían permanecer relativamente estables incluso si sus implementaciones cambian, y debería estar probando a nivel de interfaz (aunque me doy cuenta de que el mundo real a menudo no lo es) Me gusta eso ...: - /) Si sus interfaces cambian de manera importante, preferiría eliminar la mayoría o todas estas pruebas específicas de errores en lugar de reescribirlas.
j_random_hacker
@j_random_hacker Sí, por supuesto que la legibilidad es importante. Las pruebas son una forma de documentación y son tan importantes como el código de producción. Estoy de acuerdo en que descartar las pruebas para cambios importantes es algo bueno (tm) y que las pruebas deben realizarse a nivel de interfaz.
Johnno Nolan
19

Todo debe hacerse lo más simple posible, pero no más simple. - A. Einstein

Una de las cosas más incomprendidas sobre TDD es la primera palabra que contiene. Prueba. Por eso apareció BDD. Porque la gente realmente no entendía que la primera D era la importante, es decir, Driven. Todos tendemos a pensar demasiado en las pruebas y un poco o poco en la conducción del diseño. Y supongo que esta es una respuesta vaga a su pregunta, pero probablemente debería considerar cómo manejar su código, en lugar de lo que realmente está probando; eso es algo con lo que una herramienta de cobertura puede ayudarlo. El diseño es un tema bastante mayor y más problemático.

kitofr
fuente
Sí, es vago ... ¿Significa esto que como un constructor no es un comportamiento parcial, no deberíamos probarlo? Pero debería estar probando MyClass.DoSomething ()?
Johnno Nolan
Bueno, depende de: P ... una prueba de construcción suele ser un buen comienzo cuando se intenta probar código heredado. Pero probablemente (en la mayoría de los casos) dejaría fuera una prueba de construcción al comenzar a diseñar algo desde cero.
kitofr
Es desarrollo impulsado, no diseño impulsado. Es decir, obtenga una línea de base que funcione, escriba pruebas para verificar la funcionalidad, avance con el desarrollo. Casi siempre escribo mis pruebas justo antes de factorizar algún código por primera vez.
Evan Plaice
Yo diría que la última D, Diseño, es la palabra que la gente olvida, perdiendo así el foco. En el diseño basado en pruebas, escribe código en respuesta a pruebas fallidas. Si está haciendo un diseño basado en pruebas, ¿con cuánto código no probado terminará?
sheldonh
15

Para aquellos que proponen probar "todo": comprendan que "probar completamente" un método como este int square(int x)requiere alrededor de 4 mil millones de casos de prueba en lenguajes comunes y entornos típicos.

De hecho, es incluso peor que eso: un método void setX(int newX)también está obligado no alterar los valores de cualquier otro miembro, además de x- estás probando que obj.y, obj.z, etc., todos permanecen sin cambios después de llamar obj.setX(42);?

Solo es práctico probar un subconjunto de "todo". Una vez que acepta esto, se vuelve más aceptable considerar no probar un comportamiento increíblemente básico. Cada programador tiene una distribución de probabilidad de ubicaciones de errores; el enfoque inteligente es concentrar su energía en las regiones de prueba donde estima que la probabilidad de errores es alta.

j_random_hacker
fuente
9

La respuesta clásica es "pruebe cualquier cosa que pueda romperse". Lo interpreto en el sentido de que probar establecedores y captadores que no hacen nada excepto establecer u obtener es probablemente demasiada prueba, no es necesario tomarse el tiempo. A menos que su IDE los escriba por usted, también podría hacerlo.

Si su constructor no establece las propiedades puede generar errores más adelante, probar que están configuradas no es una exageración.

Dennis S.
fuente
sí, y esto es un enlace para una clase con muchas propiedades y muchos constructores.
Johnno Nolan
Cuanto más trivial sea un problema (como olvidar iniciar un miembro en cero), más tiempo llevará depurarlo.
Lev
5

Escribo pruebas para cubrir los supuestos de las clases que escribiré. Las pruebas hacen cumplir los requisitos. Básicamente, si x nunca puede ser 3, por ejemplo, me aseguraré de que haya una prueba que cubra ese requisito.

Invariablemente, si no escribo una prueba para cubrir una condición, surgirá más tarde durante la prueba "humana". Ciertamente escribiré uno entonces, pero prefiero verlos temprano. Creo que el punto es que las pruebas son tediosas (quizás) pero necesarias. Escribo suficientes pruebas para estar completo, pero nada más.

itsmatt
fuente
5

Parte del problema de omitir pruebas simples ahora es que en el futuro la refactorización podría hacer que esa propiedad simple sea muy complicada con mucha lógica. Creo que la mejor idea es que puede utilizar Tests para verificar los requisitos del módulo. Si cuando pasa X debe recuperar Y, entonces eso es lo que quiere probar. Luego, cuando cambie el código más adelante, puede verificar que X le da Y, y puede agregar una prueba para A le da B, cuando ese requisito se agregue más adelante.

Descubrí que el tiempo que dedico durante las pruebas de escritura de desarrollo inicial vale la pena en la primera o segunda corrección de errores. La capacidad de recoger el código que no ha mirado en 3 meses y estar razonablemente seguro de que su solución cubre todos los casos, y "probablemente" no rompe nada es muy valiosa. También encontrará que las pruebas unitarias ayudarán a clasificar los errores más allá del seguimiento de la pila, etc. Ver cómo funcionan y fallan las piezas individuales de la aplicación brinda una gran comprensión de por qué funcionan o fallan en su conjunto.

Mate
fuente
4

En la mayoría de los casos, yo diría que si hay lógica, pruébela. Esto incluye constructores y propiedades, especialmente cuando se establece más de una cosa en la propiedad.

Con respecto a demasiadas pruebas, es discutible. Algunos dirían que todo debe probarse para verificar su robustez, otros dicen que para una prueba eficiente, solo se deben probar las cosas que podrían romperse (es decir, la lógica).

Me inclinaría más hacia el segundo campamento, solo por experiencia personal, pero si alguien decidiera probar todo, no diría que fue demasiado ... quizás un poco exagerado para mí, pero no demasiado para ellos.

Entonces, no, yo diría que no existe tal cosa como "demasiadas" pruebas en el sentido general, solo para individuos.

Freír
fuente
3

El desarrollo basado en pruebas significa que deja de codificar cuando pasan todas sus pruebas.

Si no tiene una prueba para una propiedad, ¿por qué debería implementarla? Si no prueba / define el comportamiento esperado en caso de una asignación "ilegal", ¿qué debe hacer la propiedad?

Por lo tanto, estoy totalmente a favor de probar cada comportamiento que una clase debería exhibir. Incluidas las propiedades "primitivas".

Para facilitar esta prueba, creé un NUnit simple TestFixtureque proporciona puntos de extensión para establecer / obtener el valor y toma listas de valores válidos e inválidos y tiene una sola prueba para verificar si la propiedad funciona correctamente. Probar una sola propiedad podría verse así:

[TestFixture]
public class Test_MyObject_SomeProperty : PropertyTest<int>
{

    private MyObject obj = null;

    public override void SetUp() { obj = new MyObject(); }
    public override void TearDown() { obj = null; }

    public override int Get() { return obj.SomeProperty; }
    public override Set(int value) { obj.SomeProperty = value; }

    public override IEnumerable<int> SomeValidValues() { return new List() { 1,3,5,7 }; }
    public override IEnumerable<int> SomeInvalidValues() { return new List() { 2,4,6 }; }

}

Usando lambdas y atributos, esto podría incluso escribirse de manera más compacta. Tengo entendido que MBUnit tiene incluso algo de soporte nativo para cosas como esa. Sin embargo, el punto es que el código anterior captura la intención de la propiedad.

PD: Probablemente PropertyTest también debería tener una forma de verificar que otras propiedades en el objeto no hayan cambiado. Hmm ... de vuelta a la mesa de dibujo.

David Schmitt
fuente
Fui a una presentación sobre mbUnit. Se ve genial.
Johnno Nolan
Pero David, déjame preguntarte: ¿te sorprendió la respuesta de Kent Beck arriba? ¿Su respuesta le hace preguntarse si debería reconsiderar su enfoque? No porque alguien tenga "respuestas de lo alto", por supuesto. Pero se piensa que Kent es uno de los principales defensores de la prueba primero. ¡Un centavo por tus pensamientos!
Charlie Flowers
@Charly: La respuesta de Kent es muy pragmática. Estoy "solo" trabajando en un proyecto en el que estaré integrando código de varias fuentes y me gustaría brindar un nivel muy alto de confianza.
David Schmitt
Dicho esto, me esfuerzo por tener pruebas que sean más simples que el código probado y este nivel de detalle solo puede valer la pena en las pruebas de integración donde todos los generadores, módulos, reglas de negocio y validadores se unen.
David Schmitt
1

Hago prueba unitaria para alcanzar la máxima cobertura factible. Si no puedo alcanzar algún código, refactorizo ​​hasta que la cobertura sea lo más completa posible

Después de terminar la prueba de escritura cegadora, generalmente escribo un caso de prueba que reproduce cada error

Estoy acostumbrado a separar entre pruebas de código y pruebas de integración. Durante las pruebas de integración (que también son pruebas unitarias pero en grupos de componentes, por lo que no son exactamente para qué sirven las pruebas unitarias), probaré para que los requisitos se implementen correctamente.

Lorenzo Boccaccia
fuente
1

Entonces, cuanto más manejo mi programación escribiendo pruebas, menos me preocupo por el nivel de granualidad de las pruebas. Mirando hacia atrás, parece que estoy haciendo lo más simple posible para lograr mi objetivo de validar el comportamiento . Esto significa que estoy generando una capa de confianza de que mi código está haciendo lo que le pido, sin embargo, esto no se considera una garantía absoluta de que mi código esté libre de errores. Siento que el equilibrio correcto es probar el comportamiento estándar y tal vez uno o dos casos extremos y luego pasar a la siguiente parte de mi diseño.

Acepto que esto no cubrirá todos los errores y utilizaré otros métodos de prueba tradicionales para capturarlos.

Johnno Nolan
fuente
0

Por lo general, empiezo de a poco, con entradas y salidas que sé que deben funcionar. Luego, a medida que soluciono errores, agrego más pruebas para asegurarme de que se prueben las cosas que he solucionado. Es orgánico y me funciona bien.

¿Puedes probar demasiado? Probablemente, pero probablemente sea mejor pecar de cauteloso en general, aunque dependerá de cuán crítica sea su aplicación.

Tim Sullivan
fuente
0

Creo que debe probar todo en el "núcleo" de su lógica empresarial. Getter ans Setter también porque podrían aceptar un valor negativo o un valor nulo que es posible que no desee aceptar. Si tiene tiempo (siempre depende de su jefe), es bueno probar otra lógica de negocios y todos los controladores que llaman a estos objetos (pasa lentamente de la prueba unitaria a la prueba de integración).

Patrick Desjardins
fuente
0

No realizo pruebas unitarias con métodos simples de establecimiento / obtención que no tienen efectos secundarios. Pero hago pruebas unitarias con todos los demás métodos públicos. Intento crear pruebas para todas las condiciones de contorno en mis algoritmos y verifico la cobertura de mis pruebas unitarias.

Es mucho trabajo pero creo que vale la pena. Prefiero escribir código (incluso código de prueba) que recorrer el código en un depurador. Encuentro que el ciclo de compilación-implementación-depuración de código requiere mucho tiempo y cuanto más exhaustivas son las pruebas unitarias que he integrado en mi compilación, menos tiempo dedico a pasar por ese ciclo de compilación-implementación-depuración de código.

No dijiste por qué también estás codificando arquitectura. Pero para Java uso Maven 2 , JUnit , DbUnit , Cobertura y EasyMock .

Brian Matthews
fuente
No dije cuál, ya que es una pregunta bastante independiente del idioma.
Johnno Nolan
Las pruebas unitarias en TDD no solo lo cubren mientras escribe el código, sino que también lo protege contra la persona que inherente su código y luego piensa que tiene sentido formatear un valor dentro del getter.
Paxic
0

Cuanto más leo sobre él, más creo que algunas pruebas unitarias son como algunos patrones: un olor a lenguajes insuficientes.

Cuando necesita probar si su captador trivial realmente devuelve el valor correcto, es porque puede mezclar el nombre del captador y el nombre de la variable miembro. Ingrese 'attr_reader: name' de ruby, y esto ya no puede suceder. Simplemente no es posible en Java.

Si su getter alguna vez se vuelve no trivial, aún puede agregar una prueba para él.


fuente
Estoy de acuerdo en que probar un getter es trivial. Sin embargo, puedo ser lo suficientemente estúpido como para olvidarme de configurarlo dentro de un constructor. Por lo tanto, se necesita una prueba. Mis pensamientos han cambiado desde que hice la pregunta. Vea mi respuesta stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/…
Johnno Nolan
1
En realidad, diría que, de alguna manera, las pruebas unitarias en su conjunto son un olor a problema de lenguaje. Los lenguajes que admiten contratos (condiciones previas / posteriores a los métodos) como Eiffel, aún necesitan algunas pruebas unitarias, pero necesitan menos. En la práctica, incluso los contratos simples facilitan la localización de errores: cuando se rompe el contrato de un método, el error suele estar en ese método.
Damien Pollet
@Damien: ¿Quizás las pruebas unitarias y los contratos son realmente lo mismo disfrazado? Lo que quiero decir es que un lenguaje que "admite" contratos básicamente hace que sea fácil escribir fragmentos de código (pruebas) que se ejecutan (opcionalmente) antes y después de otros fragmentos de código, ¿correcto? Si su gramática es lo suficientemente simple, un lenguaje que no admite contratos de forma nativa puede extenderse fácilmente para admitirlos escribiendo un preprocesador, ¿correcto? ¿O hay algunas cosas que un enfoque (contratos o pruebas unitarias) puede hacer y que el otro simplemente no puede?
j_random_hacker
0

Prueba el código fuente que te preocupa.

No es útil probar partes de código en las que está muy seguro, siempre y cuando no cometa errores.

Pruebe las correcciones de errores, para que sea la primera y la última vez que corrija un error.

Pruebe para obtener confianza en partes de código oscuras, de modo que cree conocimiento.

Pruebe antes de la refactorización pesada y media, para no romper las características existentes.

castillo1971
fuente
0

Esta respuesta es más para averiguar cuántas pruebas unitarias usar para un método dado que sabe que desea probar debido a su criticidad / importancia. Utilizando la técnica Basis Path Testing de McCabe, puede hacer lo siguiente para tener una mayor confianza en la cobertura del código cuantitativamente que la simple "cobertura de declaraciones" o "cobertura de sucursales":

  1. Determine el valor de Complejidad Ciclomática de su método que desea probar unitariamente (Visual Studio 2010 Ultimate, por ejemplo, puede calcular esto por usted con herramientas de análisis estático; de lo contrario, puede calcularlo manualmente mediante el método de diagrama de flujo: http://users.csc. calpoly.edu/~jdalbey/206/Lectures/BasisPathTutorial/index.html )
  2. Enumere el conjunto básico de rutas independientes que fluyen a través de su método; consulte el enlace anterior para ver un ejemplo de diagrama de flujo
  3. Prepare pruebas unitarias para cada ruta de base independiente determinada en el paso 2
JD
fuente
¿Vas a hacer esto para todos los métodos? ¿Seriamente?
Kristopher Johnson