¿Son aceptables los números mágicos en las pruebas unitarias si los números no significan nada?

57

En mis pruebas de unidad, a menudo lanzo valores arbitrarios a mi código para ver qué hace. Por ejemplo, si sé que foo(1, 2, 3) debe devolver 17, podría escribir esto:

assertEqual(foo(1, 2, 3), 17)

Estos números son puramente arbitrarios y no tienen un significado más amplio (no son, por ejemplo, condiciones de contorno, aunque también los pruebo). Me costaría encontrar buenos nombres para estos números, y escribir algo como const int TWO = 2; es obviamente inútil. ¿Está bien escribir las pruebas como esta o debo factorizar los números en constantes?

En ¿Todos los números mágicos se crean de la misma forma? , aprendimos que los números mágicos están bien si el significado es obvio en el contexto, pero en este caso los números en realidad no tienen ningún significado.

    
pregunta Kevin 20.06.2016 - 01:01

11 respuestas

81

¿Cuándo realmente tienes números que no tienen ningún significado?

Por lo general, cuando los números tienen algún significado, debe asignarlos a las variables locales del método de prueba para que el código sea más legible y autoexplicativo. Los nombres de las variables deben al menos reflejar lo que significa la variable, no necesariamente su valor.

Ejemplo:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

Tenga en cuenta que la primera variable no se llama HUNDRED_DOLLARS_ZERO_CENT , pero startBalance para indicar cuál es el significado de la variable pero no que su valor sea de alguna manera especial.

    
respondido por el Philipp 20.06.2016 - 01:04
20

Si estás usando números arbitrarios solo para ver lo que hacen, lo que realmente estás buscando es probablemente datos de prueba generados aleatoriamente, o pruebas basadas en propiedades.

Por ejemplo, Hipótesis es una excelente biblioteca de Python para este tipo de pruebas, y se basa en QuickCheck .

  

Piense en una prueba de unidad normal como algo similar a lo siguiente:

     
  1. Configura algunos datos.
  2.   
  3. Realizar algunas operaciones en los datos.
  4.   
  5. Afirmar algo sobre el resultado.
  6.   

La hipótesis le permite escribir pruebas que, en cambio, se parecen a esto:

     
  1. Para todos los datos que coinciden con alguna especificación.
  2.   
  3. Realizar algunas operaciones en los datos.
  4.   
  5. Afirmar algo sobre el resultado.
  6.   

La idea es no limitarse a sus propios valores, sino elegir valores aleatorios que puedan usarse para verificar que sus funciones coincidan con sus especificaciones. Como nota importante, estos sistemas generalmente recordarán cualquier entrada que falle, y luego asegurarán que esas entradas siempre se prueben en el futuro.

El punto 3 puede ser confuso para algunas personas, así que aclaremos. No significa que esté afirmando la respuesta exacta, esto es obviamente imposible de hacer para una entrada arbitraria. En su lugar, afirma algo sobre una propiedad del resultado. Por ejemplo, puede afirmar que después de agregar algo a una lista no se queda vacío, o que un árbol de búsqueda binaria con equilibrio automático en realidad está equilibrado (utilizando cualquier criterio que tenga la estructura de datos en particular).

En general, elegir números arbitrarios por ti mismo es probablemente bastante malo: en realidad no agrega un montón de valor y es confuso para cualquiera que lo lea. La generación automática de un montón de datos de prueba aleatorios y su uso eficaz es bueno. Encontrar una biblioteca tipo Hipótesis o QuickCheck para el idioma de su elección es probablemente una mejor manera de lograr sus objetivos y, al mismo tiempo, ser comprensible para los demás.

    
respondido por el Dannnno 20.06.2016 - 04:56
11

El nombre de la prueba de tu unidad debe proporcionar la mayor parte del contexto. No a partir de los valores de las constantes. El nombre / la documentación de una prueba debe proporcionar el contexto y la explicación adecuados de los números mágicos presentes en la prueba.

Si eso no es suficiente, un poco de documentación debería poder proporcionarla (ya sea a través del nombre de la variable o una cadena de documentación). Tenga en cuenta que la función en sí tiene parámetros que se espera que tengan nombres significativos. Copiarlos en tu prueba para nombrar los argumentos es bastante inútil.

Y por último, si tus pruebas de unidad son lo suficientemente complicadas como para que esto sea difícil / no práctico, probablemente tengas funciones demasiado complicadas y podrías considerar por qué este es el caso.

Cuanto más descuidadamente escriba las pruebas, peor será el código real. Si siente la necesidad de asignar un nombre a sus valores de prueba para aclarar la prueba, se sugiere que su método real necesita una mejor denominación y / o documentación. Si encuentra la necesidad de nombrar constantes en las pruebas, vería por qué necesita esto: es probable que el problema no sea la prueba en sí, sino la implementación

    
respondido por el enderland 20.06.2016 - 04:39
9

Esto depende en gran medida de la función que está probando. Conozco muchos casos en los que los números individuales no tienen un significado especial por sí solos, pero el caso de prueba en su conjunto se construye de manera reflexiva y, por lo tanto, tiene un significado específico. Eso es lo que uno debería documentar de alguna manera. Por ejemplo, si foo es realmente un método testForTriangle que decide si los tres números podrían ser longitudes válidas de los bordes de un triángulo, sus pruebas podrían tener este aspecto:

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

y así sucesivamente. Puede mejorar esto y convertir los comentarios en un parámetro de mensaje de assertEqual que se mostrará cuando la prueba falle. Luego puede mejorar esto y refactorizarlo en una prueba basada en datos (si su marco de prueba lo admite). Sin embargo, usted mismo se hace un favor si coloca una nota en el código por la que eligió estos números y cuál de los diversos comportamientos que está evaluando en cada caso.

Por supuesto, para otras funciones, los valores individuales de los parámetros pueden ser más importantes, por lo que probablemente no sea la mejor idea usar un nombre de función sin sentido como foo cuando se pregunta cómo tratar el significado de los parámetros. / p>     

respondido por el Doc Brown 20.06.2016 - 22:32
6

¿Por qué queremos usar constantes con nombre en lugar de números?

  1. DRY: si necesito el valor en 3 lugares, solo quiero definirlo una vez, así que puedo cambiarlo en un lugar, si cambia.
  2. Dar significado a los números.

Si escribe varias pruebas de unidad, cada una con un surtido de 3 números (inicio de equilibrio, intereses, años): simplemente empaquetaría los valores en la prueba de unidad como variables locales. El ámbito más pequeño al que pertenecen.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

Si usa un lenguaje que permite parámetros con nombre, esto es, por supuesto, superfluo. Allí simplemente empaquetaría los valores en bruto en la llamada al método. No puedo imaginar ninguna refactorización haciendo esta declaración más concisa:

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

O use un marco de prueba, que le permitirá definir los casos de prueba en algún formato de matriz o mapa:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }
    
respondido por el Falco 20.06.2016 - 14:35
3
  

... pero en este caso los números en realidad no tienen ningún significado

Los números se utilizan para llamar a un método, por lo que seguramente la premisa anterior es incorrecta. No puede cuidar cuáles son los números, pero eso no viene al caso. Sí, podría inferir para qué se usan los números con alguna hechicería del IDE, pero sería mucho mejor si solo le diera nombres a los valores, incluso si solo coinciden con los parámetros.

    
respondido por el Robbie Dee 20.06.2016 - 10:05
3

Si desea probar una función pura en un conjunto de entradas que no son condiciones de contorno, entonces casi Ciertamente, quiero probarlo en un grupo entero de conjuntos de entradas que no son (y son) condiciones de contorno. Y para mí, eso significa que debería haber una tabla de valores para llamar a la función con, y un bucle:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Herramientas como las sugeridas en La respuesta de Dannnno puede ayudarlo a construir la tabla de valores para probar. bar , baz y blurf deben reemplazarse por nombres significativos como se explica en la respuesta de Philipp .

(Principio general discutible aquí: los números no siempre son "números mágicos" que necesitan nombres; en su lugar, los números pueden ser datos . Si tuviera sentido colocar sus números en una matriz, tal vez una La matriz de registros, entonces probablemente son datos. Por el contrario, si sospecha que podría tener datos en sus manos, considere colocarlos en una matriz y adquirir más.)

    
respondido por el zwol 20.06.2016 - 21:53
1

Primero, acordemos que la "prueba de unidad" se usa a menudo para cubrir todas las pruebas automáticas que escribe un programador, y que no tiene sentido debatir cómo debería llamarse cada prueba ...

He trabajado en un sistema donde el software tomó muchas entradas y resolvió una "solución" que tenía que cumplir algunas restricciones, mientras optimizaba otros números. No hubo respuestas correctas, por lo que el software solo tuvo que dar una respuesta razonable.

Hizo esto usando muchos números aleatorios para obtener un punto de partida, luego usó un "escalador" para mejorar el resultado. Esto se ejecutó muchas veces, escogiendo el mejor resultado. Se puede sembrar un generador de números aleatorios, de modo que siempre dé los mismos números en el mismo orden, por lo tanto, si la prueba establece una semilla, sabemos que el resultado sería el mismo en cada ejecución.

Tuvimos muchas pruebas que hacían lo anterior y verificamos que los resultados eran los mismos, esto nos dijo que no habíamos cambiado lo que esa parte del sistema hizo por error al refactorizar, etc. No nos dijo nada sobre la corrección de lo que hizo esa parte del sistema.

Estas pruebas fueron costosas de mantener, ya que cualquier cambio en el código de optimización interrumpiría las pruebas, pero también encontraron algunos errores en el código mucho más grande que preprocesó los datos y procesó los resultados posteriormente.

Como nos "burlamos" de la base de datos, podría llamar a estas pruebas "pruebas unitarias", pero la "unidad" era bastante grande.

A menudo, cuando estás trabajando en un sistema sin pruebas, haces algo como lo anterior, para que puedas confirmar que tu refactorización no cambia la salida; Esperemos que se escriban mejores pruebas para el nuevo código!

    
respondido por el Ian 20.06.2016 - 13:19
1

Creo que en este caso, los números deben denominarse Números arbitrarios, en lugar de Números mágicos, y solo comentan la línea como "caso de prueba arbitrario".

Claro, algunos Números Mágicos también pueden ser arbitrarios, como valores únicos de "manejo" (que deben reemplazarse con constantes nombradas, por supuesto), pero también pueden ser constantes precalculadas como "velocidad aérea de un gorrión europeo sin carga en furlongs por quincena ", donde el valor numérico se conecta sin comentarios ni contexto útil.

    
respondido por el RufusVS 22.06.2016 - 17:11
0

Las pruebas son diferentes del código de producción y, al menos en las pruebas de unidades escritas en Spock, que son breves y al punto, no tengo problemas al usar constantes mágicas.

Si una prueba tiene una longitud de 5 líneas y sigue el esquema básico dado / cuándo / luego, la extracción de dichos valores en constantes solo haría que el código fuera más largo y más difícil de leer. Si la lógica es "Cuando agrego un usuario llamado Smith, veo al usuario que Smith devolvió en la lista de usuarios", no tiene sentido extraer "Smith" a una constante.

Por supuesto, esto se aplica si puede hacer coincidir fácilmente los valores utilizados en el bloque "dado" (configuración) con los que se encuentran en los bloques "cuándo" y "luego". Si la configuración de su prueba está separada (en código) del lugar donde se usan los datos, es posible que sea mejor utilizar constantes. Pero dado que las pruebas son mejor autocontenidas, la configuración es generalmente cercana al lugar de uso y se aplica el primer caso, lo que significa que las constantes mágicas son bastante aceptables en este caso.

    
respondido por el Michał Kosmulski 20.06.2016 - 13:24
0

No me atreveré a decir un sí / no definitivo, pero aquí hay algunas preguntas que debe hacerse cuando decida si está bien o no.

  1. Si los números no significan nada, ¿por qué están allí en primer lugar? ¿Pueden ser reemplazados por algo más? ¿Se puede realizar una verificación basada en llamadas de método y flujo en lugar de aserciones de valor? Considere algo como el método verify() de Mockito que verifica si se realizaron ciertas llamadas de método a los objetos simulados en lugar de afirmar un valor.

  2. Si los números do significan algo, entonces deben asignarse a variables que tengan el nombre apropiado.

  3. Escribir el número 2 como TWO podría ser útil en ciertos contextos, y no tanto en otros contextos.

    • Por ejemplo: assertEquals(TWO, half_of(FOUR)) tiene sentido para alguien que lee el código. Inmediatamente queda claro qué estás probando.
    • Si, sin embargo, su prueba es assertEquals(numCustomersInBank(BANK_1), TWO) , esto no tiene mucho sentido para que . ¿Por qué BANK_1 contiene dos clientes? ¿Qué estamos probando?
respondido por el Arnab Datta 21.06.2016 - 14:02

Lea otras preguntas en las etiquetas