Estoy tratando de hacer mi código más robusto y he estado leyendo sobre pruebas unitarias, pero me resulta muy difícil encontrar un uso útil real. Por ejemplo, el ejemplo de Wikipedia :
public class TestAdder {
public void testSum() {
Adder adder = new AdderImpl();
assert(adder.add(1, 1) == 2);
assert(adder.add(1, 2) == 3);
assert(adder.add(2, 2) == 4);
assert(adder.add(0, 0) == 0);
assert(adder.add(-1, -2) == -3);
assert(adder.add(-1, 1) == 0);
assert(adder.add(1234, 988) == 2222);
}
}
Creo que esta prueba es totalmente inútil, ya que se requiere que calcules manualmente el resultado deseado y lo pruebes, siento que una prueba unitaria mejor aquí sería
assert(adder.add(a, b) == (a+b));
pero esto solo codifica la función en sí misma en la prueba. ¿Alguien puede proporcionarme un ejemplo en el que las pruebas unitarias sean realmente útiles? Para su información, actualmente estoy codificando principalmente funciones "de procedimiento" que toman ~ 10 booleanos y algunas entradas y me dan un resultado int basado en esto, siento que la única prueba de unidad que podría hacer sería simplemente volver a codificar el algoritmo en el prueba. editar: también debería haber precisado que esto es al portar (posiblemente mal diseñado) código ruby (que no hice)
fuente
How does unit testing work?
Nadie sabe realmente :)Respuestas:
Las pruebas unitarias, si está probando unidades lo suficientemente pequeñas, siempre afirman lo cegadoramente obvio.
La razón por la que
add(x, y)
incluso se menciona una prueba de unidad, es porque en algún momento alguien entraráadd
y colocará un código especial de manejo de lógica de impuestos sin darse cuenta de que el complemento se usa en todas partes.Las pruebas unitarias son muy mucho sobre el principio asociativo: si A hace B, y B hace C, entonces A hace C. "A hace C" es una prueba de nivel más alto. Por ejemplo, considere el siguiente código comercial completamente legítimo:
A primera vista, parece un método increíble para realizar pruebas unitarias, porque tiene un propósito muy claro. Sin embargo, hace alrededor de 5 cosas diferentes. Cada cosa tiene un caso válido e inválido, y hará una gran permutación de las pruebas unitarias. Idealmente, esto se desglosa aún más:
Ahora estamos en unidades. Una prueba unitaria no le importa para qué se
_userRepo
considera un comportamiento válidoFetchValidUser
, solo que se llama. Puede usar otra prueba para asegurarse exactamente de lo que constituye un usuario válido. De manera similar paraCheckUserForRole
... ha desacoplado su prueba de saber cómo se ve la estructura de Roles. También ha desacoplado todo su programa de estar estrictamente vinculadoSession
. Me imagino que todas las piezas que faltan aquí se verían así:Al refactorizar ha logrado varias cosas a la vez. El programa es mucho más compatible con la eliminación de estructuras subyacentes (puede deshacerse de la capa de base de datos para NoSQL), o agregar bloqueo sin problemas una vez que se da cuenta de
Session
que no es seguro para subprocesos o lo que sea. También se ha realizado pruebas muy directas para escribir para estas tres dependencias.Espero que esto ayude :)
fuente
Estoy bastante seguro de que cada una de sus funciones de procedimiento es determinista, por lo que devuelve un resultado int específico para cada conjunto de valores de entrada. Idealmente, tendría una especificación funcional a partir de la cual puede determinar qué resultado debe recibir para ciertos conjuntos de valores de entrada. En ausencia de eso, puede ejecutar el código ruby (que se supone que funciona correctamente) para ciertos conjuntos de valores de entrada y registrar los resultados. Luego, debe CÓDIGO DURO los resultados en su prueba. Se supone que la prueba es una prueba de que su código realmente produce resultados que se sabe que son correctos .
fuente
Como nadie más parece haber proporcionado un ejemplo real:
Tu dices:
Pero el punto es que es mucho menos probable que cometas un error (o al menos más probable que notes tus errores) al hacer los cálculos manuales que al codificar.
¿Qué posibilidades hay de cometer un error en el código de conversión de decimal a romano? Muy probable ¿Qué posibilidades hay de que cometas un error al convertir a mano números decimales a romanos? No muy probable. Es por eso que probamos contra cálculos manuales.
¿Qué posibilidades hay de cometer un error al implementar una función de raíz cuadrada? Muy probable ¿Qué posibilidades hay de que cometas un error al calcular una raíz cuadrada a mano? Probablemente más probable. Pero con sqrt, puede usar una calculadora para obtener las respuestas.
Así que voy a especular sobre lo que está sucediendo aquí. Sus funciones son un poco complicadas, por lo que es difícil determinar a partir de las entradas cuál debería ser la salida. Para hacer eso, debe ejecutar manualmente (en su cabeza) la función para descubrir cuál es la salida. Es comprensible que parezca inútil y propenso a errores.
La clave es que desea encontrar las salidas correctas. Pero debe probar esas salidas contra algo que se sabe que es correcto. No es bueno escribir su propio algoritmo para calcular eso porque eso puede ser incorrecto. En este caso, es demasiado difícil calcular manualmente los valores.
Volvería al código ruby y ejecutaría estas funciones originales con varios parámetros. Tomaría los resultados del código ruby y los pondría en la prueba de la unidad. De esa manera no tienes que hacer el cálculo manual. Pero estás probando contra el código original. Eso debería ayudar a mantener los resultados iguales, pero si hay errores en el original, entonces no lo ayudará. Básicamente, puede tratar el código original como la calculadora en el ejemplo sqrt.
Si mostró el código real que está transfiriendo, podríamos brindarle comentarios más detallados sobre cómo abordar el problema.
fuente
Estás casi en lo correcto para una clase tan simple.
Pruébelo para una calculadora más compleja. Como una calculadora de puntuación de bolos.
El valor de las pruebas unitarias se ve más fácilmente cuando tiene reglas "comerciales" más complejas con diferentes escenarios para probar.
No digo que no deba probar una calculadora de ejecución de la fábrica (¿su calculadora tiene problemas con valores como 1/3 que no se pueden representar? ¿Qué hace con la división por cero?) Pero verá el valore más claramente si prueba algo con más ramas para obtener cobertura.
fuente
A pesar del fanatismo religioso sobre el 100% de cobertura de código, diré que no todos los métodos deben ser probados en unidades. Solo funcionalidad que contiene lógica empresarial significativa. Una función que simplemente agrega número no tiene sentido probar.
Ahí está tu verdadero problema. Si las pruebas unitarias parecen anormalmente difíciles o inútiles, es probable que se deba a un defecto de diseño. Si estuviera más orientado a objetos, las firmas de su método no serían tan masivas y habría menos entradas posibles para probar.
No necesito entrar en mi OO es superior a la programación de procedimientos ...
fuente
En mi punto de vista, las pruebas unitarias son incluso útiles en su pequeña clase de sumador: no piense en "recodificar" el algoritmo y piense en él como una caja negra con el único conocimiento que tiene sobre el comportamiento funcional (si está familiarizado) con la multiplicación rápida, conoces algunos intentos más rápidos, pero más complejos que usar "a * b") y tu interfaz pública. Entonces deberías preguntarte "¿Qué demonios podría salir mal?" ...
En la mayoría de los casos, ocurre en el borde (veo que la prueba ya agrega estos patrones ++, -, + -, 00 - tiempo para completarlos por - +, 0+, 0-, +0, -0). Piense en lo que sucede en MAX_INT y MIN_INT al sumar o restar (sumar negativos;)) allí. O trate de asegurarse de que sus pruebas se vean exactamente exactamente lo que sucede en y alrededor de cero.
En general, el secreto es muy simple (quizás también para los más complejos;)) para clases simples: piense en los contratos de su clase (vea el diseño por contrato) y luego compárelos con ellos. Cuanto mejor conozca sus invitaciones, pre y post, más completados serán sus exámenes.
Sugerencia para sus clases de prueba: intente escribir solo una afirmación en un método. Dé a los métodos buenos nombres (por ejemplo, "testAddingToMaxInt", "testAddingTwoNegatives") para obtener los mejores comentarios cuando su prueba falla después del cambio de código.
fuente
En lugar de probar un valor de retorno calculado manualmente, o duplicar la lógica en la prueba para calcular el valor de retorno esperado, pruebe el valor de retorno de una propiedad esperada.
Por ejemplo, si desea probar un método que invierte una matriz, no desea invertir manualmente su valor de entrada, debe multiplicar el valor de retorno por la entrada y verificar que obtiene la matriz de identidad.
Para aplicar este enfoque a su método, deberá considerar su propósito y semántica, para identificar qué propiedades tendrá el valor de retorno en relación con las entradas.
fuente
Las pruebas unitarias son una herramienta de productividad. Recibe una solicitud de cambio, la implementa y luego ejecuta su código a través del gambito de prueba de la unidad. Esta prueba automatizada ahorra tiempo.
I feel that this test is totally useless, because you are required to manually compute the wanted result and test it, I feel like a better unit test here would be
Un punto discutible. La prueba en el ejemplo solo muestra cómo crear una instancia de una clase y ejecutarla a través de una serie de pruebas. Al concentrarse en las minucias de una sola implementación, falta el bosque para los árboles.
Can someone provide me with an example where unit testing is actually useful?
Tienes una entidad de empleado. La entidad contiene un nombre y una dirección. El cliente decide agregar un campo ReportsTo.
Esa es una prueba básica del BL para trabajar con un empleado. El código pasará / fallará el cambio de esquema que acaba de hacer. Recuerde que las afirmaciones no son lo único que hace la prueba. La ejecución del código también garantiza que no se generen excepciones.
Con el tiempo, tener las pruebas en su lugar hace que sea más fácil hacer cambios en general. El código se prueba automáticamente en busca de excepciones y contra las afirmaciones que realice. Esto evita gran parte de los gastos generales incurridos por las pruebas manuales de un grupo de control de calidad. Si bien la interfaz de usuario sigue siendo bastante difícil de automatizar, las otras capas son generalmente muy fáciles suponiendo que use los modificadores de acceso correctamente.
I feel like the only unit testing I could do would be to simply re-code the algorithm in the test.
Incluso la lógica de procedimiento se encapsula fácilmente dentro de una función. Encapsular, instanciar y pasar en el int / primitivo para ser probado (u objeto simulado). No copie y pegue el código en una Prueba de unidad. Eso derrota a DRY. También anula la prueba por completo porque no está probando el código, sino una copia del código. Si el código que debería haber sido probado cambia, ¡la prueba aún pasa!
fuente
Tomando tu ejemplo (con un poco de refactorización),
no ayuda a:
math.add
comporta internamenteEs casi como decir:
math.add
puede contener cientos de LOC; consulte a continuación).Esto también significa que no tiene que agregar pruebas como:
Tampoco ayudan, o al menos, una vez que hizo la primera afirmación, la segunda no aporta nada útil.
En cambio, ¿qué pasa con:
Esto se explica por sí mismo y es muy útil tanto para usted como para la persona que mantendrá el código fuente más adelante. Imagine que esta persona realiza una ligera modificación para
math.add
simplificar el código y optimizar el rendimiento, y ve el resultado de la prueba como:esta persona comprenderá de inmediato que el método recién modificado depende del orden de los argumentos: si el primer argumento es un número entero y el segundo es un número largo, el resultado sería un número entero, mientras que se esperaba un número largo.
Del mismo modo, obtener el valor real de
4.141592
en la primera afirmación se explica por sí mismo: usted sabe que se espera que el método tenga una gran precisión , pero en realidad falla.Por la misma razón, dos afirmaciones siguientes pueden tener sentido en algunos idiomas:
Además, ¿qué pasa con:
También se explica por sí mismo: desea que su método pueda manejar correctamente el infinito. Ir más allá del infinito o lanzar una excepción no es un comportamiento esperado.
¿O tal vez, dependiendo de tu idioma, esto tendrá más sentido?
fuente
Para una función muy simple como agregar, las pruebas podrían considerarse innecesarias, pero a medida que sus funciones se vuelven más complejas, se vuelve cada vez más obvio por qué es necesaria la prueba.
Piensa en lo que haces cuando estás programando (sin pruebas unitarias). Por lo general, escribe un código, lo ejecuta, ve que funciona y pasa a lo siguiente, ¿verdad? A medida que escribe más código, especialmente en un sistema / GUI / sitio web muy grande, descubre que debe hacer más y más "correr y ver si funciona". Tienes que probar esto y probar eso. Luego, haces algunos cambios y tienes que probar esas mismas cosas una y otra vez. Se vuelve muy obvio que podría ahorrar tiempo escribiendo pruebas unitarias que automatizarían toda la parte de "ejecutar y ver si funciona".
A medida que sus proyectos se hacen cada vez más grandes, la cantidad de cosas que tiene que "ejecutar y ver si funciona" se vuelve poco realista. Entonces terminas simplemente ejecutando y probando algunos componentes principales de la GUI / proyecto y luego esperando que todo lo demás esté bien. Esta es una receta para el desastre. Por supuesto, usted, como ser humano, no puede probar repetidamente cada situación posible que sus clientes podrían usar si la GUI es utilizada literalmente por cientos de personas. Si tenía pruebas unitarias en su lugar, simplemente podría ejecutar la prueba antes de enviar la versión estable, o incluso antes de comprometerse con el repositorio central (si su lugar de trabajo usa una). Y, si se encuentran errores más adelante, puede agregar una prueba unitaria para verificarlo en el futuro.
fuente
Una de las ventajas de escribir pruebas unitarias es que te ayuda a escribir código más robusto al obligarte a pensar en casos extremos. ¿Qué hay de probar algunos casos límite, como desbordamiento de enteros, truncamiento decimal o manejo de valores nulos para los parámetros?
fuente
Tal vez asuma que add () se implementó con la instrucción ADD. Si algún programador junior o ingeniero de hardware reimplementa la función add () usando ANDS / ORS / XORS, bits invertidos y cambios, es posible que desee probar la unidad contra la instrucción ADD.
En general, si reemplaza las agallas de add (), o la unidad bajo prueba, con un número aleatorio o un generador de salida, ¿cómo podría saber que algo está roto? Codifique ese conocimiento en sus pruebas unitarias. Si nadie puede saber si está roto, simplemente ingrese un código para rand () e ir a casa, su trabajo está hecho.
fuente
Podría haberlo omitido en todas las respuestas, pero, para mí, el impulso principal detrás de Unit Testing se trata menos de probar la corrección de un método hoy en día, sino que demuestra la corrección continua de ese método cuando [alguna vez] lo cambias .
Tome una función simple, como devolver el número de elementos en alguna colección. Hoy, cuando su lista se basa en una estructura de datos interna que conoce bien, podría pensar que este método es tan dolorosamente obvio que no necesita una prueba para ello. Luego, en varios meses o años, usted (u otra persona ) decide reemplazar la estructura de la lista interna. Aún necesita saber que getCount () devuelve el valor correcto.
Ahí es donde sus Pruebas de Unidad realmente entran en juego
Puede cambiar la implementación interna de su código, pero para cualquier consumidor de ese código, el resultado sigue siendo el mismo.
fuente