2010-03-20 8 views
15

Me he encontrado con este dilema varias veces. ¿Deberían mis pruebas unitarias duplicar la funcionalidad del método que están probando para verificar su integridad? O ¿Deberían las pruebas unitarias tratar de probar el método con numerosas instancias de creadas manualmente de entradas y salidas esperadas?¿Debe una prueba de unidad replicar la funcionalidad o la salida de prueba?

Principalmente hago la pregunta para situaciones en las que el método que está probando es razonablemente simple y su funcionamiento correcto puede verificarse echando un vistazo al código por un minuto.

ejemplo

simplificado (en rubí):

def concat_strings(str1, str2) 
    return str1 + " AND " + str2 
end 

simplificado funcionalidad de replicación de prueba para el método anterior:

def test_concat_strings 
    10.times do 
    str1 = random_string_generator 
    str2 = random_string_generator 
    assert_equal (str1 + " AND " + str2), concat_strings(str1, str2) 
    end 
end 

entiendo que la mayoría de las veces el método que se está probando no será sencillo lo suficiente como para justificar hacerlo de esta manera. Pero mi pregunta permanece; ¿es esta una metodología válida en algunas circunstancias (por qué o por qué no)?

+0

No estoy muy familiarizado con el primer escenario, donde una prueba unitaria duplica la funcionalidad de un método, ¿puede darme un poco más de detalles? – RobS

+0

En Desarrollo controlado por prueba A menudo estoy creando una prueba para verificar que un método (aún por escribir) funcione como se espera. A menudo me siento tentado, en escenarios simples, de escribir estas especificaciones de la forma que mejor sé, de forma programática para lograr el resultado deseado a partir de los valores de entrada. A veces siento que sería más probable que cometa un error al calcular a mano y al ingresar los resultados esperados que simplemente escribir un poco de código para hacer el trabajo. –

+0

gran pregunta: supongo que esta es una excepción a la regla de OnceAndOnlyOnce .. a menos que no sepa cómo refactorizar esto correctamente. – Gishu

Respuesta

2

Es una polémica postura , pero creo que es unit testing using Derived Values muy superior al uso de entradas y salidas codificadas de manera arbitraria.

El problema es que a medida que un algoritmo se vuelve incluso ligeramente complejo, la relación entre la entrada y la salida se vuelve oscura si se representa mediante valores codificados. La prueba unitaria termina siendo un postulado . Puede funcionar técnicamente, pero perjudica la capacidad de mantenimiento de la prueba porque conduce a Obscure Tests.

El uso de Derived Values para probar el resultado establece una relación más clara entre la entrada de prueba y la salida esperada.

El argumento de que esto no prueba nada simplemente no es cierto, porque cualquier caso de prueba ejercerá solo una parte de una ruta a través del SUT, por lo que ningún caso de prueba reproducirá todo el algoritmo que se prueba, pero la combinación de pruebas lo hará.

Una ventaja adicional es que puede use fewer unit tests to cover the desired functionality, e incluso hacerlos más comunicativos al mismo tiempo. El resultado final es más terser y más pruebas de unidad que se pueden mantener.

2

En la prueba unitaria, debe encontrar casos de prueba manualmente (por lo tanto, entrada, salida y qué efectos secundarios está esperando; estas serán las expectativas en sus objetos simulados). Usted presenta estos casos de prueba de manera que cubren todas las funcionalidades de su clase (por ejemplo, todos los métodos están cubiertos, todas las ramas de todas las declaraciones if, etc.). Piénselo más en la línea de crear documentación de su clase mostrando todos los usos posibles.

La reimplementación de la clase no es una buena idea, ya que no solo obtiene una duplicación obvia de código/funcionalidad, sino que también es probable que presente los mismos errores en esta nueva implementación.

1

para probar la funcionalidad de un método usaría pares de entrada y salida siempre que sea posible. de lo contrario, podría copiar & pegando la funcionalidad y los errores en su implementación. ¿Qué estás probando entonces? estaría probando si la funcionalidad (incluidos todos sus errores) no ha cambiado con el tiempo. pero no estaría probando la corrección de la implementación.

probando si la funcionalidad no ha cambiado con el tiempo podría ser (temporalmente) útil durante la refactorización. pero ¿con qué frecuencia refactoriza esos pequeños métodos?

también las pruebas unitarias pueden verse como documentación y como especificación de las entradas de un método y las salidas esperadas. ambos deben ser lo más simples posible para que otros puedan leerlo y comprenderlo fácilmente. tan pronto como introduce código/lógica adicional en una prueba, se vuelve más difícil de leer.

su prueba se parece realmente a fuzz test. Las pruebas de fuzz pueden ser muy útiles, pero en pruebas unitarias se debe evitar la aleatoriedad debido a la reproducibilidad.

8

Al probar la funcionalidad mediante el uso de la misma implementación, no se prueba nada. Si uno tiene un error, el otro también lo hará.

Pero las pruebas comparando con una implementación alternativa son un enfoque válido. Por ejemplo, puede probar un método iterativo (rápido) para calcular los números de Fibonacci comparándolo con una aplicación trivial recursiva pero lenta del mismo método.

Una variación de esto es el uso de una implementación, que solo funciona para casos especiales. Por supuesto, en ese caso, puede usarlo solo para casos especiales.

Al elegir valores de entrada, usar valores aleatorios la mayor parte del tiempo no es muy efectivo. Prefiero valores cuidadosamente elegidos en cualquier momento. En el ejemplo que proporcionó, los valores nulos y los valores extremadamente largos que no encajarán en una Cadena cuando se concatenan vienen a la mente.

Si utiliza valores aleatorios, asegúrese de que tiene una forma de volver a crear la ejecución exacta con los mismos valores aleatorios, por ejemplo, registrando el valor de inicialización, y teniendo una manera de establecer ese valor en el momento del inicio.

1

Una prueba de unidad debe ejercer su código, no es algo que forme parte del idioma que está utilizando.

Si la lógica del código es concatenar cadenas de una manera especial, debería probarlo; de lo contrario, debe confiar en su lenguaje/marco.

Finalmente, debe crear sus pruebas unitarias para que fallen primero "con significado". En otras palabras, los valores aleatorios no deben utilizarse (a no ser que se está probando su generador de números aleatorios no devuelve el mismo conjunto de valores aleatorios!)

0

Nunca utilice datos aleatorios para la entrada. Si su prueba informa una falla, ¿cómo podrá duplicarla? Y no use la misma función para generar el resultado esperado. Si tiene un error en su método, es probable que ponga el mismo error en su prueba. Calcule los resultados esperados por algún otro método.

Los valores codificados son perfectamente finos, y asegúrese de seleccionar las entradas para representar todos los casos normales y de borde. Por lo menos, pruebe las entradas esperadas, así como las entradas en el formato incorrecto o en el tamaño incorrecto (p. Ej .: valores nulos).

Es realmente bastante simple: una prueba de unidad debe probar si la función funciona o no. Eso significa que debe proporcionar un rango de entradas conocidas que tengan salidas conocidas y que prueben en contra de eso. No hay una forma universal correcta de hacerlo. Sin embargo, usar el mismo algoritmo para el método y la verificación no prueba nada más que ser experto en copiar/pegar.

0

Sí. También me molesta ... aunque diría que es más frecuente con cálculos no triviales. Con el fin de evitar la actualización de la prueba cuando los cambios en el código, algunos programadores escribir una prueba de ISX = X, que siempre tiene éxito, independientemente de la IVU

  • Acerca de duplicar la funcionalidad

Usted no tiene a. Su prueba puede indicar cuál es el resultado esperado, no cómo lo obtuvo. Aunque en algunos casos no triviales, puede hacer que su prueba sea más legible en cuanto a cómo obtuvo el valor esperado, pruebe como una especificación. Usted no debe refactorizar esta duplicación de distancia

def doubler(x); x * 2; end 

def test_doubler() 
    input, expected = 10, doubler(10) 

    assert_equal expected, doubler(10) 
end 

Ahora si cambio de refuerzo (x) para ser un triplicador, la prueba anterior no fallará. def doubler(x); x * 3; end

Sin embargo, esto se haría:

def test_doubler() 
    assert_equal(20, doubler(10)) 
end 
  • aleatoriedad en las pruebas de unidad - no lo hacen.

En lugar de los conjuntos de datos aleatorios, elegir los puntos de datos representativos estáticas para la prueba y utilizar un xUnit RowTest/TestCase para ejecutar la prueba con entradas de datos diff. Si n conjuntos de entrada son idénticos para la unidad, elija 1. La prueba en el OP podría usarse como una prueba exploratoria/o para determinar conjuntos de entrada representativos adicionales. Las pruebas unitarias deben ser repetibles (Ver q # 61400) - Usar valores aleatorios derrota este objetivo.

+0

No estaba preguntando si volver a utilizar el mismo método exacto, sino simplemente reimplementar parte o la totalidad de la funcionalidad del SUT para generar el valor esperado para la prueba de una manera comprensible. –

+0

@ Daniel - como menciono en el segundo párrafo, para la legibilidad en las pruebas tendrías que duplicar la lógica. p.ej. 'EXPECTED_TOTAL_PRICE = lineitems.inject {| sum, item | sum + = (item.price * item.quantity)} * (1-DISCOUNT_RATE) ' – Gishu

+0

De acuerdo. Creo que malinterpreté tu primer ejemplo como una sugerencia de qué hacer, en lugar de qué no hacer :-) –