unit-testing - software - pruebas unitarias java
¿Qué hace una buena prueba de unidad? (18)
Estoy seguro de que la mayoría de ustedes está escribiendo muchas pruebas automatizadas y que también se han encontrado con algunas trampas comunes al realizar pruebas unitarias.
Mi pregunta es, ¿sigues alguna de las reglas de conducta para escribir pruebas 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 se escriben las pruebas?
Se alientan sugerencias de idioma independiente.
- No escriba pruebas descomunales. Como sugiere la ''unidad'' en ''prueba de unidad'', haga que cada una sea lo más atómica y aislada posible. Si es necesario, cree condiciones previas mediante el uso de objetos simulados, en lugar de recrear demasiado el entorno de usuario típico de forma manual.
- No pruebes cosas que obviamente funcionan. Evite probar las clases de un proveedor externo, especialmente el que suministra las API centrales del marco en el que codifica. Por ejemplo, no pruebe agregar un elemento a la clase Hashtable del proveedor.
- Considere usar una herramienta de cobertura de código como NCover para descubrir casos extremos que aún debe probar.
- Intenta escribir la prueba antes de la implementación. Piense en la prueba como una especificación más que su implementación se adherirá. Cf. también el desarrollo impulsado por el comportamiento, una rama más específica del desarrollo impulsado por pruebas.
- Se consistente. Si solo escribes pruebas para parte de tu código, es difícilmente útil. Si trabajas en equipo, y algunos o todos los demás no escriben pruebas, tampoco es muy útil. Convéncete a ti mismo y a todos los demás de la importancia (y las propiedades de ahorro de tiempo ) de las pruebas, o no te molestes.
A menudo, las pruebas unitarias se basan en objetos falsos o datos simulados. Me gusta escribir tres tipos de pruebas unitarias:
- Pruebas unitarias "transitorias": crean sus propios objetos / datos falsos y prueban su función con ellos, pero destruyen todo y no dejan rastros (como no hay datos en una base de datos de prueba)
- Prueba de unidad "persistente": prueban funciones dentro de su código creando objetos / datos que serán necesarios para funciones más avanzadas para su propia prueba unitaria (evitando que esas funciones avanzadas recreen cada vez su propio conjunto de objetos / datos falsos)
- Pruebas unitarias "basadas en la persistencia": pruebas unitarias utilizando objetos / datos falsos que ya están allí (porque se crearon en otra sesión de prueba de la unidad) mediante las pruebas unitarias persistentes.
El objetivo es evitar reproducir todo para poder probar todas las funciones.
- Ejecuto el tercer tipo con mucha frecuencia porque todos los objetos / datos falsos ya están allí.
- Ejecuto el segundo tipo cada vez que cambia mi modelo.
- Ejecuto el primero para verificar las funciones más básicas de vez en cuando, para verificar las regresiones básicas.
Apoyo la respuesta "A TRIP", ¡¡¡excepto que las pruebas DEBERÍAN apoyarse mutuamente !!!
¿Por qué?
DRY - No te repitas: ¡también se aplica a las pruebas! Las dependencias de prueba pueden ayudar a 1) guardar el tiempo de configuración, 2) guardar los recursos del dispositivo, y 3) identificar los fallos. 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/
Cubrí estos principios hace un tiempo en este artículo de MSDN Magazine, que creo que es importante que lea cualquier desarrollador.
La forma en que defino las pruebas de unidades "buenas" es si poseen las siguientes tres propiedades:
- Son legibles (nombres, afirmaciones, variables, duración, complejidad ...)
- Son mantenibles (sin lógica, no especificado, basado en estado, refactorizado ...)
- Son dignos de confianza (prueba lo correcto, aislado, no pruebas de integración ...)
Jay Fields tiene muchos buenos consejos sobre pruebas de unidad de escritura y hay una publicación donde resume los consejos más importantes . Allí leerá que debe pensar críticamente sobre su contexto y juzgar si el consejo es valioso para usted. Aquí recibe toneladas de respuestas sorprendentes, pero depende de usted decidir cuál es la mejor para su contexto. Pruébalos y simplemente refactoriza si huele mal.
Saludos cordiales
La prueba debería fallar originalmente. Luego debe escribir el código que los hace pasar, de lo contrario correrá el riesgo de escribir una prueba que se escucha y pasa siempre.
Las pruebas deben ser aisladas. Una prueba no debería depender de otra. Aún más, una prueba no debe basarse en 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.
Lo que busca es la delineación de los comportamientos de la clase bajo prueba.
- Verificación de comportamientos esperados.
- Verificación de casos de error.
- Cobertura de todas las rutas de código dentro de la clase.
- Ejercicio de todas las funciones miembro dentro de la clase.
La intención básica es aumentar tu confianza en el comportamiento de la clase.
Esto es especialmente útil cuando se trata de refacturar su código. Martin Fowler tiene un article interesante sobre pruebas en su sitio web.
HTH.
aclamaciones,
Robar
Mantenga estos objetivos en mente (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 hacer esto más fácil:
- Las pruebas solo deberían fallar por una razón.
- Las pruebas solo deberían probar una cosa
- Minimice las dependencias de prueba (sin dependencias en bases de datos, archivos, etc.)
No olvide que también puede realizar pruebas de integración con su marco xUnit, pero que las pruebas de integración y las pruebas de unidades se separen por separado.
Me gusta el acrónimo BICEP correcto del libro Pragmatic Unit Testing antes mencionado:
- Derecha : ¿Son los resultados correctos ?
- B : ¿Son correctas todas las condiciones bilaterales?
- I : ¿Podemos verificar las relaciones negativas?
- C : ¿Podemos verificar los resultados usando otros medios?
- E : ¿Podemos forzar que ocurran condiciones de riesgo?
- P : ¿Están las características de rendimiento dentro de los 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 frontera 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 agregar) y forzar condiciones de error tales como fallas de red.
Nunca asuma que un método trivial de 2 líneas funcionará. Escribir una prueba unitaria rápida es la única manera de evitar que la prueba nula faltante, el signo menos extraviado y / o el error de alcance sutil te muerdan, inevitablemente cuando tienes incluso menos tiempo para lidiar con eso que ahora.
Permítanme comenzar conectando fuentes - Pruebas unitarias pragmáticas en Java con JUnit (Hay una versión con C # -Nunit también ... pero tengo esta ... es, en su mayoría, agnóstico. Recomendado.)
Las buenas pruebas deben ser UN VIAJE (el acrónimo no es suficientemente adhesivo; tengo una copia impresa de la hoja de prueba en el libro que tuve que sacar para asegurarme de que lo entendí bien ...)
- Automático : la invocación de pruebas y la comprobación de resultados de PASS / FAIL 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 clave y escenarios. Use herramientas si debe conocer las regiones no probadas.
- Repetible : las pruebas deben producir los mismos resultados cada vez ... siempre. Las pruebas no deben basarse en parametros incontrolables.
- Independiente : Muy importante.
- Las pruebas deberían probar solo una cosa a la vez. Las múltiples afirmaciones están bien siempre y cuando todas estén probando una característica / comportamiento. Cuando una prueba falla, debe señalar la ubicación del problema.
- Las pruebas no deben basarse entre sí : aisladas. No hay suposiciones sobre el orden de ejecución de la prueba. Asegúrese de ''borrón y cuenta nueva'' antes de cada prueba mediante el uso de configuración / desmontaje adecuadamente
Profesional : A la larga, tendrá el mismo código de prueba que la 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 bien factorizados: clases 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 tome más de medio segundo para ejecutarse ... necesita ser trabajada. Cuanto más tiempo demore el banco de pruebas en ejecutarse, menos se ejecutará. Cuantos más cambios el desarrollador intente escabullirse entre las carreras ... si algo se rompe ... tomará más tiempo descubrir qué cambio fue el culpable.
Actualización 2010-08:
- Legible : esto se puede considerar 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 al igual que el código de producción, por lo que es más fácil leerlas aunque requiera más esfuerzo. Las pruebas deben ser simétricas (seguir un patrón) y concisas (probar un comportamiento a la vez). Use una convención de nomenclatura coherente (p. Ej., El estilo TestDox). Evite saturar la prueba con "detalles incidentales" ... conviértase en un minimalista.
Además de estos, la mayoría de los otros son pautas que reducen el trabajo de bajo beneficio: por ejemplo, "No pruebes el código que no posees" (por ejemplo, archivos DLL de terceros). No intentes probar getters y setters. Esté atento a la relación costo-beneficio o probabilidad de defecto.
Piense en los 2 tipos de pruebas y trátelas de manera diferente: pruebas funcionales y pruebas de rendimiento.
Use diferentes entradas y métricas para cada uno. Es posible que necesite usar un software diferente para cada tipo de prueba.
Uso una convención de nomenclatura de prueba consistente descrita por los estándares de Naming Test Name de Roy Osherove. Cada método en una clase de caso de prueba dada tiene el siguiente estilo de nombre 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 son los resultados de ese escenario.
Cada sección utiliza el caso Camel superior y está delimitada por un puntaje inferior.
He encontrado 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 entender el intento de prueba.
También agrego parámetros al nombre del Método si el método bajo prueba se ha sobrecargado.
Algunas propiedades de grandes pruebas unitarias:
Cuando falla una prueba, debería ser inmediatamente obvio dónde radica el problema. Si tiene que usar el depurador para rastrear el problema, entonces sus pruebas no son lo suficientemente detalladas. Tener exactamente una afirmación por prueba ayuda aquí.
Cuando refactorices, ninguna prueba debería fallar.
Las pruebas deben ejecutarse tan rápido que nunca dude en ejecutarlas.
Todas las pruebas deben pasar siempre; no hay resultados no deterministas.
Las pruebas unitarias deben tener un buen factor, 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 las personas externas que llaman. (Sin embargo, si siento la necesidad de escribir pruebas para métodos privados, entonces tengo que 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 múltiples afirmaciones al respecto, pero solo tiene una afirmación por prueba, puede duplicar la configuración en múltiples pruebas.
No tomo ese enfoque. En cambio, uso accesorios de prueba por escenario . Aquí hay 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...
}
}
Las buenas pruebas deben ser mantenibles.
No he descifrado cómo hacer esto para entornos complejos.
Todos los libros de texto comienzan a despegarse a medida que su base de códigos comienza a llegar a los cientos de 1000 o millones de líneas de código.
- Las interacciones del equipo explotan
- número de casos de prueba explotan
- interacciones entre componentes explota.
- el tiempo para construir todos los unittest se convierte en una parte importante del tiempo de compilación
- un cambio de API puede afectar a cientos de casos de prueba. Aunque el cambio del código de producción fue fácil.
- la cantidad de eventos requeridos 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 interacción, pero inevitablemente a medida que los sistemas se vuelven más complejos, el sistema de prueba automatizado crece con ella.
Aquí es donde comienzas a tener que lidiar con compensaciones:
- solo prueba la API externa; de otro modo, la refactorización de los resultados internos da como resultado una repetición significativa de las pruebas.
- la configuración y desmontaje de cada prueba se vuelve más complicada ya que un subsistema encapsulado conserva más estado.
- la compilación nocturna y la ejecución de prueba automatizada aumentan a horas.
- un aumento en los tiempos de compilación y ejecución significa que los diseñadores no ejecutan o no ejecutarán todas las pruebas
- para reducir los tiempos de ejecución de la prueba, considere la posibilidad de realizar pruebas de secuencia para reducir la configuración y el desmontaje
También debes decidir:
¿Dónde almacena casos de prueba en su código base?
- ¿Cómo documentas tus casos de prueba?
- ¿Se pueden reutilizar los dispositivos de prueba para guardar el mantenimiento del caso de prueba?
- ¿Qué sucede cuando falla la ejecución de un caso de prueba nocturno? ¿Quién hace el triage?
- ¿Cómo mantienes los objetos simulados? Si tiene 20 módulos, todos usan su propio sabor de una API de registro simulada, cambiando las ondulaciones de la API rápidamente. Los casos de prueba no solo cambian sino que 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.
- las personas y sus equipos entienden el valor de las pruebas automatizadas, simplemente no les gusta cómo lo está haciendo el otro equipo. :-)
Podría continuar para siempre, pero mi punto es que:
Las pruebas deben ser mantenibles.
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 ellos mismos (cómo). Dado que la pregunta parecía bastante específica en la parte de "cómo", pensé en publicar esto, tomado de una presentación de "bolsa marrón" que realicé en mi empresa.
Las 5 leyes de pruebas de escritura de Womp:
1. Use nombres largos y descriptivos de métodos de prueba.
- Map_DefaultConstructorShouldCreateEmptyGisMap()
- ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
- Dog_Object_Should_Eat_Homework_Object_When_Hungry()
2. Escriba sus pruebas en un estilo Arrange / Act / Assert .
- Si bien esta estrategia organizacional ha existido por un tiempo y ha llamado a muchas cosas, la introducción del acrónimo "AAA" recientemente ha sido una excelente manera de transmitir esto. Hacer que todas tus pruebas sean consistentes con el estilo AAA las hace fáciles de leer y mantener.
3. Siempre proporcione un mensaje de falla con su Asserts.
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 obvio en tu aplicación de corredor lo que ha fallado. Si no proporciona un mensaje, generalmente obtendrá algo como "Esperado verdadero, fue falso" en su resultado de falla, lo que hace que realmente tenga que leer la prueba para averiguar qué está mal.
4. Comenta el motivo de la prueba : ¿cuál es la suposición comercial?
/// 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 las personas que no entienden 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 burlas siempre que sea posible para evitar el manejo de recursos reales.
- La limpieza debe hacerse a nivel de prueba. Las pruebas no deben depender del orden de ejecución.
- Las Pruebas Unitarias simplemente prueban la API externa de su Unidad, no debe probar el comportamiento interno.
- Cada prueba de un TestCase debe probar uno (y solo uno) método dentro de esta API.
- Casos de prueba adicionales deben incluirse para los casos de falla.
- Pruebe la cobertura de sus pruebas: una vez que se prueba la unidad, se debe haber ejecutado el 100% de las líneas dentro de esta unidad.