¿Qué hace una buena prueba unitaria? [cerrado]


97

Estoy seguro de que la mayoría de ustedes está escribiendo muchas pruebas automatizadas y que también se han encontrado con algunos errores comunes al realizar pruebas unitarias.

Mi pregunta es, ¿sigue alguna regla de conducta para redactar exámenes para evitar problemas en el futuro? Para ser más específico: ¿Cuáles son las propiedades de las buenas pruebas unitarias o cómo escribes tus pruebas?

Se alientan las sugerencias independientes del idioma.

Respuestas:


93

Permítanme comenzar conectando fuentes: pruebas unitarias pragmáticas en Java con JUnit (también hay una versión con C # -Nunit ... pero tengo esta ... es agnóstica en su mayor parte. Recomendado).

Las buenas pruebas deben ser UN VIAJE (el acrónimo no es lo suficientemente pegajoso; tengo una copia impresa de la hoja de referencia en el libro que tuve que sacar para asegurarme de que entendí bien ...)

  • Automático : la invocación de pruebas y la verificación de resultados para PASA / NO PASA deben ser automáticas
  • Completo : Cobertura; Aunque los errores tienden a agruparse alrededor de ciertas regiones en el código, asegúrese de probar todas las rutas y escenarios clave. Utilice herramientas si debe conocer las regiones no probadas
  • Repetible : las pruebas deben producir los mismos resultados cada vez ... siempre. Las pruebas no deben depender de parámetros incontrolables.
  • Independiente : Muy importante.
    • Las pruebas deben probar solo una cosa a la vez. Varias afirmaciones están bien siempre que todas estén probando una característica / comportamiento. Cuando una prueba falla, debe identificar la ubicación del problema.
    • Las pruebas no deben depender entre sí : aisladas. Sin suposiciones sobre el orden de ejecución de la prueba. Asegúrese de "borrón y cuenta nueva" antes de cada prueba utilizando la configuración / desmontaje de forma adecuada
  • Profesional : a largo plazo, tendrá tanto código de prueba como producción (si no más), por lo tanto, siga el mismo estándar de buen diseño para su código de prueba. Métodos-clases bien factorizados con nombres que revelan la intención, sin duplicación, pruebas con buenos nombres, etc.

  • Las buenas pruebas también se ejecutan rápido . cualquier prueba que tarde más de medio segundo en ejecutarse ... necesita ser trabajada. Cuanto más tiempo tarde en ejecutarse el conjunto de pruebas, con menor frecuencia se ejecutará. Cuantos más cambios intente el desarrollador de escabullirse entre ejecuciones ... si algo se rompe ... llevará más tiempo descubrir qué cambio fue el culpable.

Actualización 2010-08:

  • Legible : esto puede considerarse parte de Professional, sin embargo, no se puede enfatizar lo suficiente. Una prueba de fuego sería encontrar a alguien que no sea parte de su equipo y pedirle que descubra el comportamiento bajo prueba en un par de minutos. Las pruebas deben mantenerse como el código de producción, por lo que debe facilitar su lectura incluso si requiere más esfuerzo. Las pruebas deben ser simétricas (seguir un patrón) y concisas (probar un comportamiento a la vez). Utilice una convención de nomenclatura coherente (por ejemplo, el estilo TestDox). Evite abarrotar la prueba con "detalles incidentales" ... conviértase en un minimalista.

Aparte de estos, la mayoría de los otros son pautas que reducen el trabajo de bajo beneficio: por ejemplo, "No pruebe el código que no es de su propiedad" (por ejemplo, DLL de terceros). No vayas a probar getters y setters. Esté atento a la relación costo-beneficio o la probabilidad de defectos.


Puede que no estemos de acuerdo con el uso de Mocks, pero esta fue una descripción muy buena de las mejores prácticas de pruebas unitarias.
Justin Standard

Voy a aumentar este como respuesta entonces porque encuentro útil el acrónimo "A TRIP".
Spoike

3
Estoy de acuerdo en su mayor parte, pero me gustaría señalar que hay un beneficio en probar código que no es de tu propiedad ... Estás probando que cumple con tus requisitos. ¿De qué otra manera puede estar seguro de que una actualización no dañará sus sistemas? (Pero, por supuesto, tenga en cuenta la relación costo / beneficio al hacerlo).
Desilusionado

@Craig: creo que te refieres a las pruebas de regresión (a nivel de interfaz) (o pruebas de aprendizaje en algunos casos), que documentan el comportamiento del que dependes. No escribiría pruebas de 'unidad' para código de terceros porque a. el proveedor sabe más sobre ese código que yo b. El proveedor no está obligado a conservar ninguna implementación específica. No controlo los cambios en esa base de código y no quiero perder mi tiempo arreglando pruebas rotas con una actualización. Así que prefiero codificar algunas pruebas de regresión de alto nivel para el comportamiento que uso (y quiero ser notificado cuando se rompa)
Gishu

@Gishu: ¡Sí, absolutamente! Las pruebas solo deben realizarse a nivel de interfaz; y de hecho, como máximo debería probar las funciones que realmente utiliza. Además, a la hora de elegir con qué escribir estas pruebas; He descubierto que los marcos de prueba sencillos y sencillos de 'unidad' generalmente se ajustan perfectamente a la factura.
Desilusionado

42
  1. No escriba pruebas descomunales. Como sugiere la 'unidad' en 'prueba unitaria', haga que cada uno sea lo más atómico y aislado posible. Si es necesario, cree condiciones previas utilizando objetos simulados, en lugar de recrear demasiado del entorno de usuario típico manualmente.
  2. No pruebes cosas que obviamente funcionan. Evite probar las clases de un proveedor externo, especialmente el que proporciona las API principales del marco en el que codifica. Por ejemplo, no pruebe agregar un elemento a la clase Hashtable del proveedor.
  3. Considere usar una herramienta de cobertura de código como NCover para ayudar a descubrir casos extremos que aún no ha probado.
  4. Intente escribir la prueba antes de la implementación. Piense en la prueba como una especificación a la que se adherirá su implementación. Cf. también desarrollo impulsado por el comportamiento, una rama más específica del desarrollo impulsado por pruebas.
  5. Se consistente. Si solo escribe pruebas para parte de su código, difícilmente será útil. Si trabajas en equipo y algunos o todos los demás no escriben pruebas, tampoco es muy útil. Convénzase a sí mismo y a todos los demás de la importancia (y las propiedades de ahorro de tiempo ) de las pruebas, o no se moleste.

1
Buena respuesta. Pero no es tan malo si no realiza una prueba unitaria para todo en una entrega. Seguro que es preferible, pero debe haber equilibrio y pragmatismo. Re: conseguir que sus colegas se unan; a veces solo necesita hacerlo para demostrar valor y como punto de referencia.
Martin Clarke

1
Estoy de acuerdo. Sin embargo, a la larga, debe poder confiar en que las pruebas están allí, es decir, ser capaz de asumir que las trampas comunes serán atrapadas por ellas. De lo contrario, los beneficios se reducen enormemente.
Sören Kuklau

2
"Si solo escribe pruebas para parte de su código, no es útil". ¿Es este realmente el caso? Tengo proyectos con una cobertura de código del 20% (áreas cruciales / propensas a fallar) y me ayudaron enormemente, y los proyectos también están bien.
dr. malvado

1
Estoy de acuerdo con Slough. Incluso si solo hay unas pocas pruebas, dado que están bien redactadas y lo suficientemente aisladas, serán de gran ayuda.
Spoike

41

La mayoría de las respuestas aquí parecen abordar las mejores prácticas de pruebas unitarias en general (cuándo, dónde, por qué y qué), en lugar de escribir las pruebas en sí mismas (cómo). Dado que la pregunta parecía bastante específica en la parte "cómo", pensé en publicar esto, tomado de una presentación de "bolsa marrón" que realicé en mi empresa.

Las 5 leyes de las pruebas de escritura de Womp:


1. Utilice nombres de métodos de prueba largos y descriptivos.

   - Map_DefaultConstructorShouldCreateEmptyGisMap()
   - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
   - Dog_Object_Should_Eat_Homework_Object_When_Hungry()

2. Escriba sus pruebas en un estilo Organizar / Actuar / Afirmar .

  • Si bien esta estrategia organizacional ha existido por un tiempo y se ha llamado muchas cosas, la introducción del acrónimo "AAA" recientemente ha sido una excelente manera de transmitir esto. Hacer que todas sus pruebas sean coherentes con el estilo AAA las hace fáciles de leer y mantener.

3. Siempre proporcione un mensaje de error con sus afirmaciones.

Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element 
processing events was raised by the XElementSerializer");
  • Una práctica simple pero gratificante que hace evidente en su aplicación de corredor lo que ha fallado. Si no proporciona un mensaje, generalmente obtendrá algo como "Se esperaba verdadero, era falso" en su salida de falla, lo que hace que tenga que leer la prueba para averiguar qué está mal.

4. Comente el motivo de la prueba : ¿cuál es el supuesto empresarial?

  /// A layer cannot be constructed with a null gisLayer, as every function 
  /// in the Layer class assumes that a valid gisLayer is present.
  [Test]
  public void ShouldNotAllowConstructionWithANullGisLayer()
  {
  }
  • Esto puede parecer obvio, pero esta práctica protegerá la integridad de sus pruebas de personas que no comprenden la razón detrás de la prueba en primer lugar. He visto eliminar o modificar muchas pruebas que estaban perfectamente bien, simplemente porque la persona no entendía las suposiciones que la prueba estaba verificando.
  • Si la prueba es trivial o el nombre del método es suficientemente descriptivo, puede permitirse dejar el comentario.

5. Cada prueba siempre debe revertir el estado de cualquier recurso que toque.

  • Utilice simulacros siempre que sea posible para evitar tratar con recursos reales.
  • La limpieza debe realizarse a nivel de prueba. Las pruebas no deben depender del orden de ejecución.

2
+1 porque los puntos 1, 2 y 5 son importantes. 3 y 4 parecen bastante excesivos para las pruebas unitarias, si ya está utilizando nombres de métodos de prueba descriptivos, pero recomiendo la documentación de las pruebas si son de gran alcance (pruebas funcionales o de aceptación).
Spoike

+1 para conocimientos y ejemplos prácticos y prácticos
Phil

17

Tenga en cuenta estos objetivos (adaptado del libro xUnit Test Patterns de Meszaros)

  • Las pruebas deben reducir el riesgo, no introducirlo.
  • Las pruebas deben ser fáciles de ejecutar.
  • Las pruebas deben ser fáciles de mantener a medida que el sistema evoluciona a su alrededor.

Algunas cosas para facilitar esto:

  • Las pruebas solo deben fallar por una razón.
  • Las pruebas solo deben probar una cosa
  • Minimice las dependencias de prueba (sin dependencias en bases de datos, archivos, interfaz de usuario, etc.)

No olvide que también puede hacer pruebas de integración con su marco xUnit, pero mantenga las pruebas de integración y las pruebas unitarias separadas


Supongo que quisiste decir que has adaptado del libro "xUnit Test Patterns" de Gerard Meszaros. xunitpatterns.com
Spoike

Sí, tienes razón. Lo
aclararé

Excelentes puntos. Las pruebas unitarias pueden ser muy útiles, pero es muy importante evitar caer en la trampa de tener pruebas unitarias complejas e interdependientes que crean un impuesto enorme para cualquier intento de cambiar el sistema.
Wedge

9

Las pruebas deben estar aisladas. Una prueba no debería depender de otra. Además, una prueba no debe depender de sistemas externos. En otras palabras, pruebe su código, no el código del que depende su código. Puede probar esas interacciones como parte de su integración o pruebas funcionales.


9

Algunas propiedades de las grandes pruebas unitarias:

  • Cuando una prueba falla, debería ser inmediatamente obvio dónde radica el problema. Si tiene que usar el depurador para rastrear el problema, sus pruebas no son lo suficientemente detalladas. Tener exactamente una afirmación por prueba ayuda aquí.

  • Cuando refactoriza, ninguna prueba debe fallar.

  • Las pruebas deben ejecutarse tan rápido que nunca dude en ejecutarlas.

  • Todas las pruebas deben pasar siempre; sin resultados no deterministas.

  • Las pruebas unitarias deben estar bien factorizadas, al igual que su código de producción.

@Alotor: Si está sugiriendo que una biblioteca solo debería tener pruebas unitarias en su API externa, no estoy de acuerdo. Quiero pruebas unitarias para cada clase, incluidas las clases que no expongo a llamadas externas. (Sin embargo, si siento la necesidad de escribir pruebas para métodos privados, entonces necesito refactorizar ) .


EDITAR: Hubo un comentario sobre la duplicación causada por "una afirmación por prueba". Específicamente, si tiene algún código para configurar un escenario y luego desea hacer varias afirmaciones al respecto, pero solo tiene una afirmación por prueba, puede duplicar la configuración en varias pruebas.

Yo no adopto ese enfoque. En su lugar, utilizo accesorios de prueba por escenario . He aquí un ejemplo aproximado:

[TestFixture]
public class StackTests
{
    [TestFixture]
    public class EmptyTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
        }

        [TestMethod]
        [ExpectedException (typeof(Exception))]
        public void PopFails()
        {
            _stack.Pop();
        }

        [TestMethod]
        public void IsEmpty()
        {
            Assert(_stack.IsEmpty());
        }
    }

    [TestFixture]
    public class PushedOneTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
            _stack.Push(7);
        }

        // Tests for one item on the stack...
    }
}

No estoy de acuerdo con una sola afirmación por prueba. Cuantas más afirmaciones tenga en una prueba, menos casos de prueba de cortar y pegar tendrá. Creo que un caso de prueba debe centrarse en un escenario o ruta de código y las afirmaciones deben provenir de todos los supuestos y requisitos para cumplir con ese escenario.
Lucas B

Creo que estamos de acuerdo en que DRY se aplica a las pruebas unitarias. Como dije, "las pruebas unitarias deben estar bien factorizadas". Sin embargo, hay varias formas de resolver la duplicación. Una, como mencionas, es tener una prueba unitaria que primero invoca el código bajo prueba y luego afirma varias veces. Una alternativa es crear un nuevo "dispositivo de prueba" para el escenario, que invoca el código bajo prueba durante un paso de inicialización / configuración, y luego tiene una serie de pruebas unitarias que simplemente afirman.
Jay Bazuzi

Mi regla general es que si usa copiar y pegar, está haciendo algo mal. Uno de mis dichos favoritos es "Copiar y pegar no es un patrón de diseño". También estoy de acuerdo en que una afirmación por unidad de prueba es generalmente una buena idea, pero no siempre insisto en ello. Me gusta la prueba más general de "prueba una cosa por unidad". Aunque eso generalmente se traduce en una afirmación por prueba unitaria.
Jon Turner

7

Lo que busca es delinear los comportamientos de la clase bajo prueba.

  1. Verificación de comportamientos esperados.
  2. Verificación de casos de error.
  3. Cobertura de todas las rutas de código dentro de la clase.
  4. Ejercer todas las funciones miembro dentro de la clase.

La intención básica es aumentar su confianza en el comportamiento de la clase.

Esto es especialmente útil cuando se busca refactorizar su código. Martin Fowler tiene un artículo interesante sobre pruebas en su sitio web.

HTH.

salud,

Robar


Rob: mecánico, esto es bueno, pero pierde la intención. ¿Por qué hiciste todo esto? Pensar de esta manera puede ayudar a otros en el camino de TDD.
Mark Levison

7

La prueba debería fallar originalmente. Luego, debe escribir el código que los hace pasar, de lo contrario, corre el riesgo de escribir una prueba que tiene errores y siempre pasa.


@Rismo No exclusivo per se. Por definición, lo que Quarrelsome escribió aquí es exclusivo de la metodología "Test First", que es parte de TDD. TDD también tiene en cuenta la refactorización. La definición más "sabelotodo" que he leído es que TDD = Test First + Refactor.
Spoike

Sí, no tiene que ser TDD, solo asegúrese de que su prueba falle primero. Luego, cablee el resto después. Esto ocurre con más frecuencia cuando se utiliza TDD, pero también puede aplicarlo cuando no se utiliza TDD.
Quisquilloso

6

Me gusta el acrónimo Right BICEP del libro Pragmatic Unit Testing antes mencionado :

  • Derecha : ¿Son correctos los resultados ?
  • B : Son todas las b condiciones oundary corregir?
  • I : ¿Podemos comprobar yo? nverse relaciones?
  • C : ¿Podemos c resultados Ross-verificación utilizando otros medios?
  • E : ¿Podemos forzar e condiciones RROR a pasar?
  • P : ¿Son p características ENDIMIENTO dentro de límites?

Personalmente, creo que puede llegar bastante lejos comprobando que obtiene los resultados correctos (1 + 1 debería devolver 2 en una función de suma), probando todas las condiciones de contorno que pueda imaginar (como usar dos números de los cuales la suma es mayor que el valor máximo entero en la función de suma) y obliga a condiciones de error como fallas en la red.


6

Las buenas pruebas deben ser mantenibles.

No he descubierto muy bien cómo hacer esto para entornos complejos.

Todos los libros de texto comienzan a deshacerse a medida que su código base comienza a llegar a los cientos de miles o millones de líneas de código.

  • Las interacciones de equipo explotan
  • número de casos de prueba explota
  • las interacciones entre los componentes explotan.
  • el tiempo para construir todas las pruebas unitarias se convierte en una parte importante del tiempo de construcción
  • un cambio de API puede extenderse a cientos de casos de prueba. Aunque el cambio de código de producción fue fácil.
  • el número de eventos necesarios para secuenciar procesos en el estado correcto aumenta, lo que a su vez aumenta el tiempo de ejecución de la prueba.

Una buena arquitectura puede controlar parte de la explosión de la interacción, pero inevitablemente a medida que los sistemas se vuelven más complejos, el sistema de pruebas automatizado crece con ellos.

Aquí es donde comienza a tener que lidiar con las compensaciones:

  • Solo pruebe la API externa; de lo contrario, la refactorización interna da como resultado una reelaboración significativa del caso de prueba.
  • La configuración y el desmontaje de cada prueba se vuelve más complicado a medida que un subsistema encapsulado retiene más estado.
  • La compilación nocturna y la ejecución de pruebas automatizadas aumentan a horas.
  • El aumento de los tiempos de compilación y ejecución significa que los diseñadores no ejecutarán o no ejecutarán todas las pruebas.
  • para reducir los tiempos de ejecución de las pruebas, considere la posibilidad de secuenciar las pruebas para reducir la configuración y el desmontaje

También debes decidir:

¿Dónde almacena los casos de prueba en su base de código?

  • ¿Cómo documenta sus casos de prueba?
  • ¿Se pueden reutilizar los accesorios de prueba para ahorrar el mantenimiento del caso de prueba?
  • ¿Qué sucede cuando falla la ejecución de un caso de prueba nocturno? ¿Quién realiza el triaje?
  • ¿Cómo se mantienen los objetos simulados? Si tiene 20 módulos que usan todos su propio tipo de API de registro simulado, el cambio de API se propaga rápidamente. No solo cambian los casos de prueba, sino que también cambian los 20 objetos simulados. Esos 20 módulos fueron escritos durante varios años por muchos equipos diferentes. Es un problema clásico de reutilización.
  • Los individuos y sus equipos comprenden el valor de las pruebas automatizadas, simplemente no les gusta cómo lo hace el otro equipo. :-)

Podría seguir para siempre, pero mi punto es que:

Las pruebas deben ser mantenibles.


5

Abordé estos principios hace un tiempo en este artículo de la revista MSDN que creo que es importante que cualquier desarrollador lea.

La forma en que defino las pruebas unitarias "buenas" es si poseen las siguientes tres propiedades:

  • Son legibles (naming, afirma, variables, longitud, complejidad ..)
  • Son Mantenibles (sin lógica, no sobreespecificados, basados ​​en el estado, refactorizados ..)
  • Son confiables (prueben lo correcto, aisladas, no pruebas de integración ...)

Roy, estoy totalmente de acuerdo. Estas cosas son mucho más importantes que la cobertura de casos extremos.
Matt Hinze

digno de confianza - ¡excelente punto!
Ratkok

4
  • Unit Testing solo prueba la API externa de su unidad, no debe probar el comportamiento interno.
  • Cada prueba de un TestCase debe probar un método (y solo uno) dentro de esta API.
    • Se deben incluir casos de prueba adicionales para los casos de falla.
  • Pruebe la cobertura de sus pruebas: Una vez que se prueba una unidad, se debería haber ejecutado el 100% de las líneas dentro de esta unidad.


1

Nunca asuma que un método trivial de 2 líneas funcionará. Escribir una prueba unitaria rápida es la única forma de evitar que la prueba nula faltante, el signo menos fuera de lugar y / o el error de alcance sutil lo muerdan, inevitablemente cuando tiene menos tiempo para lidiar con eso que ahora.


1

Apoyo la respuesta de "UN VIAJE", excepto que las pruebas DEBEN depender unas de otras.

¿Por qué?

SECO: no se repita: también se aplica a las pruebas. Las dependencias de prueba pueden ayudar a 1) ahorrar tiempo de configuración, 2) ahorrar recursos de dispositivos y 3) identificar fallas. Por supuesto, solo dado que su marco de prueba admite dependencias de primera clase. De lo contrario, lo admito, son malos.

Seguimiento http://www.iam.unibe.ch/~scg/Research/JExample/


Estoy de acuerdo contigo. TestNG es otro marco en el que las dependencias se permiten fácilmente.
Davide

0

A menudo, las pruebas unitarias se basan en objetos simulados o datos simulados. Me gusta escribir tres tipos de pruebas unitarias:

  • Pruebas unitarias "transitorias": crean sus propios objetos / datos simulados y prueban su función con ellos, pero destruyen todo y no dejan rastro (como si no hubiera datos en una base de datos de prueba)
  • prueba unitaria "persistente": prueban funciones dentro de su código creando objetos / datos que serán necesarios para funciones más avanzadas más adelante para su propia prueba unitaria (evitando que esas funciones avanzadas recreen cada vez su propio conjunto de objetos / datos simulados)
  • Pruebas unitarias "basadas en persistencia": pruebas unitarias que utilizan objetos / datos simulados que ya están allí (porque se crearon en otra sesión de prueba unitaria) mediante las pruebas unitarias persistentes.

El punto es evitar reproducir todo para poder probar todas las funciones.

  • Ejecuto el tercer tipo muy a menudo porque todos los objetos / datos simulados ya están allí.
  • Ejecuto el segundo tipo cada vez que cambia mi modelo.
  • Ejecuto el primero para verificar las funciones básicas de vez en cuando, para verificar las regresiones básicas.

0

Piense en los 2 tipos de pruebas y trátelos de manera diferente: pruebas funcionales y pruebas de rendimiento.

Utilice diferentes entradas y métricas para cada uno. Es posible que deba utilizar un software diferente para cada tipo de prueba.


Entonces, ¿qué pasa con las pruebas unitarias?
Spoike

0

Utilizo una convención de nomenclatura de prueba consistente descrita por los estándares de nomenclatura de pruebas unitarias de Roy Osherove Cada método en una clase de caso de prueba dada tiene el siguiente estilo de nomenclatura MethodUnderTest_Scenario_ExpectedResult.

    La primera sección del nombre de la prueba es el nombre del método en el sistema bajo prueba.
    El siguiente es el escenario específico que se está probando.
    Finalmente están los resultados de ese escenario.

Cada sección usa Upper Camel Case y está delimitada por una puntuación inferior.

Encontré esto útil cuando ejecuto la prueba, la prueba se agrupa por el nombre del método bajo prueba. Y tener una convención permite a otros desarrolladores comprender la intención de la prueba.

También agrego parámetros al nombre del método si el método bajo prueba se ha sobrecargado.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.