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

58

En mis pruebas unitarias, a menudo arrojo valores arbitrarios a mi código para ver qué hace. Por ejemplo, si sé que foo(1, 2, 3)se supone que devuelve 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 las pruebo). Me gustaría encontrar buenos nombres para estos números, y escribir algo así const int TWO = 2;obviamente no es útil. ¿Está bien escribir las pruebas de esta manera, o debo factorizar los números en constantes?

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

Kevin
fuente
99
Si está poniendo valores y espera poder volver a leer esos mismos valores, diría que los números mágicos están bien. Entonces, si, por ejemplo, 1, 2, 3hay índices de matriz 3D donde previamente almacenó el valor 17, entonces creo que esta prueba sería excelente (siempre y cuando también tenga algunas pruebas negativas). Pero si es el resultado de un cálculo, debe asegurarse de que cualquiera que lea esta prueba entienda por qué foo(1, 2, 3)debería serlo 17, y los números mágicos probablemente no logren ese objetivo.
Joe White
24
const int TWO = 2;es incluso peor que solo usarlo 2. Se ajusta a la redacción de la regla con la intención de violar su espíritu.
Agent_L
44
¿Qué es un número que "no significa nada"? ¿Por qué estaría en su código si no significara nada?
Tim Grant
66
Seguro. Deje un comentario antes de una serie de tales pruebas, por ejemplo, "una pequeña selección de ejemplos determinados manualmente". Esto, en relación con sus otras pruebas que claramente están probando límites y excepciones, será claro.
davidbak
55
Su ejemplo es engañoso: cuando el nombre de su función sería realmente foo, no significaría nada y, por lo tanto, los parámetros. Pero, en realidad, estoy bastante seguro de que la función no tiene ese nombre, y los parámetros no tienen nombres bar1, bar2y bar3. Haga un ejemplo más realista donde los nombres tengan un significado, entonces tiene mucho más sentido discutir si los valores de los datos de prueba también necesitan un nombre.
Doc Brown

Respuestas:

80

¿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 fácil de explicar. 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 tiene nombre HUNDRED_DOLLARS_ZERO_CENT, pero startBalancepara indicar cuál es el significado de la variable, pero no que su valor sea de ninguna manera especial.

Philipp
fuente
3
@Kevin: ¿en qué idioma estás probando? Algunos marcos de prueba le permiten configurar proveedores de datos que devuelven una serie de matrices de valores para la prueba
HorusKol
10
Aunque estoy de acuerdo con la idea, tenga en cuenta que esta práctica también puede introducir nuevos errores, como si extrae accidentalmente un valor similar 0.05fa un int. :)
Jeff Bowman
55
+1: cosas geniales. El hecho de que no te importe cuál es un valor en particular, eso no significa que no sea un número mágico ...
Robbie Dee
2
@PieterB: AFAIK es culpa de C y C ++, que formalizó la noción de una constvariable.
Steve Jessop
2
¿Has nombrado tus variables igual que los parámetros nombrados de calculateCompoundInterest? Si es así, la escritura adicional es una prueba de trabajo de que ha leído la documentación de la función que está probando, o al menos copió los nombres que le dio su IDE. No estoy seguro de cuánto le dice esto al lector sobre la intención del código, pero si pasa los parámetros en el orden incorrecto, al menos pueden decir lo que se pretendía.
Steve Jessop
20

Si usa números arbitrarios solo para ver lo que hacen, entonces lo que realmente está 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 unitaria normal como algo similar a lo siguiente:

  1. Configurar algunos datos.
  2. Realice algunas operaciones en los datos.
  3. Afirma algo sobre el resultado.

La hipótesis le permite escribir pruebas que en cambio se ven así:

  1. Para todos los datos que coinciden con alguna especificación.
  2. Realice algunas operaciones en los datos.
  3. Afirma algo sobre el resultado.

La idea es no limitarse a sus propios valores, sino elegir valores aleatorios que se puedan usar para verificar que sus funciones coincidan con sus especificaciones. Como nota importante, estos sistemas generalmente recordarán cualquier entrada que falle y luego se asegurarán de 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 cambio, afirma algo sobre una propiedad del resultado. Por ejemplo, puede afirmar que después de agregar algo a una lista, se vuelve no vacío, o que un árbol de búsqueda binario autoequilibrado está realmente equilibrado (utilizando cualquier criterio que tenga esa estructura de datos en particular).

En general, elegir números arbitrarios usted mismo probablemente sea bastante malo: realmente no agrega un montón de valor y es confuso para cualquier otra persona que lo lea. Generar automáticamente un montón de datos de prueba aleatorios y usarlos efectivamente es bueno. Encontrar una hipótesis o una biblioteca similar a QuickCheck para su idioma de elección es probablemente una mejor manera de lograr sus objetivos sin dejar de ser comprensible para otros.

Dannnno
fuente
11
Las pruebas aleatorias pueden encontrar errores difíciles de reproducir, pero las pruebas aleatorias apenas encuentran errores reproducibles. Asegúrese de capturar cualquier falla de prueba con un caso de prueba reproducible específico.
JBRWilkinson
55
¿Y cómo sabe que su prueba de unidad no tiene errores cuando "afirma algo sobre el resultado" (en este caso, recalcule lo que fooestá computando) ...? Si estuvieras 100% seguro de que tu código da la respuesta correcta, simplemente pondrías ese código en el programa y no lo probarías. Si no lo está, entonces necesita probar la prueba, y creo que todos ven a dónde va esto.
2
Sí, si pasa entradas aleatorias a una función, debe saber cuál sería la salida para poder afirmar que funciona correctamente. Con valores de prueba fijos / elegidos, por supuesto, puede resolverlo a mano, etc., pero seguramente cualquier método automatizado para determinar si el resultado es correcto está sujeto a los mismos problemas exactos que la función que está probando. Usted usa la implementación que tiene (que no puede porque está probando si funciona) o escribe una nueva implementación que es probable que tenga errores (o más, de lo contrario, usaría la que sea más probable que sea correcta) )
Chris
77
@NajibIdrissi, no necesariamente. Podría, por ejemplo, probar que aplicar el inverso de la operación que está probando al resultado devuelve el valor inicial con el que comenzó. O puede probar los invariantes esperados (por ejemplo, para todos los cálculos de intereses en ddías, el cálculo en ddías + 1 mes debe ser una tasa de porcentaje mensual conocida más alta), etc.
Julio
12
@Chris: en muchos casos, verificar que los resultados sean correctos es más fácil que generar los resultados. Si bien esto no es cierto en todas las circunstancias, hay muchos donde está. Ejemplo: agregar una entrada a un árbol binario equilibrado debería dar como resultado un nuevo árbol que también esté equilibrado ... fácil de probar, bastante difícil de implementar en la práctica.
Jules
11

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

Si eso no es suficiente, un poco de documentación debería poder proporcionarlo (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, con suerte, tienen nombres significativos. Copiarlos en su prueba para nombrar los argumentos es bastante inútil.

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

Cuanto más descuidadamente escriba las pruebas, peor será su código real. Si siente la necesidad de nombrar sus valores de prueba para aclarar la prueba, sugiere que su método real necesita mejores nombres y / o documentación. Si encuentra la necesidad de nombrar constantes en las pruebas, investigaría por qué necesita esto: probablemente el problema no sea la prueba en sí sino la implementación

Enderland
fuente
Esta respuesta parece ser acerca de la dificultad de inferir el propósito de una prueba, mientras que la pregunta real es acerca de números mágicos en los parámetros del método ...
Robbie Dee
@RobbieDee el nombre / documentación para una prueba debe dar el contexto apropiado y la explicación de los números mágicos presentes en la prueba. De lo contrario, agregue documentación o cambie el nombre de la prueba para que quede más claro.
enderland
Todavía sería mejor dar nombres a los números mágicos. Si el número de parámetros cambiara, la documentación corre el riesgo de quedar desactualizada.
Robbie Dee
1
@RobbieDee tenga en cuenta que la función en sí misma tiene parámetros que, con suerte, tienen nombres significativos. Copiarlos en su prueba para nombrar los argumentos es bastante inútil.
enderland
"Con suerte" ¿eh? ¿Por qué no acaba de codificar adecuadamente la cosa y acabar con lo que es aparentemente un número mágico como Philipp ya ha esbozado ...
Robbie Dee
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í mismos, pero el caso de prueba en su conjunto se construye cuidadosamente y, por lo tanto, tiene un significado específico. Eso es lo que uno debe documentar de alguna manera. Por ejemplo, si foorealmente es un método testForTriangleque decide si los tres números pueden ser longitudes válidas de los bordes de un triángulo, sus pruebas podrían verse así:

// 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í. Puede mejorar esto y convertir los comentarios en un parámetro de mensaje assertEqualque se mostrará cuando la prueba falle. Luego puede mejorar esto aún más y refactorizarlo en una prueba basada en datos (si su marco de prueba lo admite). Sin embargo, te haces un favor si pones una nota en el código de por qué elegiste estos números y cuál de los diversos comportamientos que estás probando con el caso individual.

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

Doc Brown
fuente
Solución sensata.
user1725145
6

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

  1. SECO: si necesito el valor en 3 lugares, solo quiero definirlo una vez, para poder cambiarlo en un lugar, si cambia.
  2. Dar significado a los números.

Si escribe varias pruebas unitarias, cada una con un surtido de 3 Números (balance inicial, interés, años), simplemente empaquetaría los valores en la prueba unitaria como variables locales. El alcance 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 utiliza un lenguaje que permite parámetros con nombre, esto es, por supuesto, superfluo. Allí simplemente empacaría los valores sin procesar 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 utilice 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
              }
            }
Falco
fuente
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. Puede que no te importe cuáles son los números, pero eso no viene al caso. Sí, podría inferir para qué se usan los números por parte de la magia de IDE, pero sería mucho mejor si solo diera los nombres de los valores, incluso si solo coinciden con los parámetros.

Robbie Dee
fuente
1
Sin embargo, esto no es necesariamente cierto, como en el ejemplo de la prueba de unidad más reciente que escribí ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). En este ejemplo, 42es solo un valor de marcador de posición producido por el código en el script de prueba nombrado lvalue_operatorsy luego verificado cuando es devuelto por el script. No tiene ningún significado, aparte de que el mismo valor se produce en dos lugares diferentes. ¿Cuál sería un nombre apropiado aquí que real da algún significado útil?
Julio
3

Si desea probar una función pura en un conjunto de entradas que no son condiciones límite, entonces casi con certeza desea probarla en un conjunto completo de conjuntos de entradas que no son (y son) condiciones límite. Y para mí eso significa que debería haber una tabla de valores para llamar a la función 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);
}

Las herramientas como las sugeridas en la respuesta de Dannnno pueden ayudarlo a construir la tabla de valores para probar. bar, bazY blurfdebe ser sustituida 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 cambio, los números podrían ser datos . Si tuviera sentido poner sus números en una matriz, tal vez una matriz de registros, entonces probablemente sean datos Por el contrario, si sospecha que podría tener datos en sus manos, considere ponerlos en una matriz y adquirir más.)

zwol
fuente
1

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 ningún problema al usar constantes mágicas.

Si una prueba tiene 5 líneas de largo y sigue el esquema básico dado / cuándo / luego, extraer dichos valores en constantes solo haría que el código sea más largo y difícil de leer. Si la lógica es "Cuando agrego un usuario llamado Smith, veo que el usuario Smith regresó a 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 encontrados en los bloques "cuándo" y "luego". Si su configuración de prueba está separada (en código) del lugar donde se usan los datos, podría ser mejor usar constantes. Pero dado que las pruebas son mejor autónomas, la configuración suele estar cerca del lugar de uso y se aplica el primer caso, lo que significa que las constantes mágicas son bastante aceptables en este caso.

Michał Kosmulski
fuente
1

En primer lugar, aceptemos que la "prueba unitaria" se usa a menudo para cubrir todas las pruebas automatizadas que escribe un programador, y que no tiene sentido debatir cómo debe llamarse cada prueba ...

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

Lo hizo mediante el uso de muchos números aleatorios para obtener un punto de partida, y luego con un "escalador" para mejorar el resultado. Esto se ejecutó muchas veces, eligiendo 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 hicieron lo anterior, y verificamos que los resultados fueran 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 romperí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.

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

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

Ian
fuente
1

Creo que en este caso los números deberían denominarse Números Arbitrarios, en lugar de Números Mágicos, y simplemente comentar la línea como "caso de prueba arbitrario".

Claro, algunos números mágicos también pueden ser arbitrarios, en cuanto a valores únicos de "manejo" (que deberían reemplazarse con constantes con nombre, 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 o contexto útil.

RufusVS
fuente
0

No me aventuraré a decir un sí / no definitivo, pero aquí hay algunas preguntas que debe hacerse al decidir 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 otra cosa? ¿Se puede hacer una verificación basada en llamadas a métodos y flujo en lugar de afirmaciones de valor? Considere algo como el verify()método de Mockito que verifica si ciertas llamadas al método se realizaron para simular objetos en lugar de afirmar un valor.

  2. Si los números hacen significar algo, entonces deben ser asignados a las variables que se denominan de manera apropiada.

  3. Escribir el número 2que TWOpodrí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. Está claro de inmediato lo que está probando.
    • Sin embargo, si la prueba es assertEquals(numCustomersInBank(BANK_1), TWO), entonces esto no hace que mucho sentido. ¿ Por quéBANK_1 contiene dos clientes? ¿Para qué estamos probando?
Arnab Datta
fuente