2009-03-11 13 views
6

Desde que comencé a usar rspec, tuve un problema con la noción de accesorios. Mi principal preocupación es la siguiente:¿Es una mala práctica generar aleatoriamente datos de prueba?

  1. Uso las pruebas para revelar un comportamiento sorprendente. No siempre soy lo suficientemente inteligente como para enumerar todos los casos posibles para los ejemplos que estoy probando. Usar accesorios codificados parece limitado porque solo prueba mi código con los casos muy específicos que he imaginado. (Es cierto que mi imaginación también es limitante con respecto a los casos que pruebo).

  2. Utilizo las pruebas como una forma de documentación para el código. Si tengo valores de dispositivo codificados, es difícil revelar lo que una prueba en particular está tratando de demostrar. Por ejemplo:

    describe Item do 
        describe '#most_expensive' do 
        it 'should return the most expensive item' do 
         Item.most_expensive.price.should == 100 
         # OR 
         #Item.most_expensive.price.should == Item.find(:expensive).price 
         # OR 
         #Item.most_expensive.id.should == Item.find(:expensive).id 
        end 
        end 
    end 
    

    Usando el primer método proporciona al lector ninguna indicación de lo que el artículo más caro es, solo que su precio es de 100. Los tres métodos piden al lector a tomar en la fe de que el aparato es el :expensive el más caro enumerado en fixtures/items.yml. Un programador descuidado podría romper las pruebas creando un Item en before(:all), o insertando otro dispositivo en fixtures/items.yml. Si ese es un archivo grande, podría llevar mucho tiempo descubrir cuál es el problema.

Una cosa que he empezado a hacer es añadir un método #generate_random a todos mis modelos. Este método solo está disponible cuando estoy ejecutando mis especificaciones. Por ejemplo:

class Item 
    def self.generate_random(params={}) 
    Item.create(
     :name => params[:name] || String.generate_random, 
     :price => params[:price] || rand(100) 
    ) 
    end 
end 

(. Los detalles específicos de cómo hacer esto en realidad son un poco más limpio que tengo una clase que se encarga de la generación y la limpieza de todos los modelos, pero este código es lo suficientemente claro para mi ejemplo.) Entonces, en el ejemplo anterior, podría probar lo siguiente. Una advertencia para los débiles de corazón: mi código se basa principalmente en el uso de before(:all):

describe Item do 
    describe '#most_expensive' do 
    before(:all) do 
     @items = [] 
     3.times { @items << Item.generate_random } 
     @items << Item.generate_random({:price => 50}) 
    end 

    it 'should return the most expensive item' do 
     sorted = @items.sort { |a, b| b.price <=> a.price } 
     expensive = Item.most_expensive 
     expensive.should be(sorted[0]) 
     expensive.price.should >= 50  
    end 
    end 
end 

De esta manera, mis pruebas revelan un mejor comportamiento sorprendente. Cuando genero datos de esta manera, de vez en cuando tropiezo con un caso marginal en el que mi código no se comporta como se esperaba, pero que no habría captado si solo estuviera usando dispositivos. Por ejemplo, en el caso de #most_expensive, si olvidé manejar el caso especial donde varios artículos comparten el precio más caro, mi prueba ocasionalmente fallaría en el primer should. Ver las fallas no deterministas en AutoSpec me daría una pista de que algo andaba mal. Si solo estuviera usando accesorios, podría llevar mucho más tiempo descubrir tal error.

Mis pruebas también hacen un trabajo un poco mejor al demostrar en código cuál es el comportamiento esperado. Mi prueba deja en claro que ordenado es una matriz de elementos ordenados en orden descendente por precio. Como espero que #most_expensive sea igual al primer elemento de esa matriz, es aún más obvio cuál es el comportamiento esperado de most_expensive.

Entonces, ¿es esta una mala práctica? ¿Mi miedo a los accesorios es irracional? Está escribiendo demasiado un método generate_random para cada modelo? ¿O esto funciona?

+0

La línea "3.times {@items 50})" no se ve bien. –

+1

Y ahora, apenas 58 meses después, respondo ... No se ve bien porque tiene "< <" en él ... pero no se escapó correctamente. – bobocopy

Respuesta

5

Esta es una respuesta a su segundo punto:

(2) que utilizan pruebas como una forma de documentación para el código. Si tengo valores de dispositivo codificados, es difícil revelar lo que una prueba en particular está tratando de demostrar.

Estoy de acuerdo. Idealmente, los ejemplos de especificaciones deberían ser comprensibles por sí mismos. Usar accesorios es problemático porque divide las condiciones previas del ejemplo de los resultados esperados.

Debido a esto, muchos usuarios de RSpec han dejado de utilizar los dispositivos por completo. En su lugar, construya los objetos necesarios en el propio ejemplo de especificación.

describe Item, "#most_expensive" do 
    it 'should return the most expensive item' do 
    items = [ 
     Item.create!(:price => 100), 
     Item.create!(:price => 50) 
    ] 

    Item.most_expensive.price.should == 100 
    end 
end 

Si su extremo con un montón de código estándar para la creación de objetos, se debe echar un vistazo a algunas de las muchas bibliotecas de fábrica de objetos de prueba, tales como factory_girl, Machinist o FixtureReplacement.

+0

¿Está roto el enlace FixtureReplacement? –

+0

Muchas respuestas excelentes, pero esta es la mejor manera de hacer lo que quiero hacer, y mis datos de prueba ya no tienen que ser 'aleatorios'. – bobocopy

+0

bobocopy: Parece que sí. Extraño, creo que estaba funcionando ayer. Está arreglado ahora. –

0

Un problema con los casos de prueba generados al azar es que la validación de la respuesta debe ser calculado por código y no se puede estar seguro de que no tiene errores :)

+0

Las pruebas y el código se prueban entre sí. Si su prueba tiene errores, lo sabrá rápidamente. :) –

4

Pensamos mucho sobre esto en un proyecto mío reciente. Al final, nos decidimos por dos puntos:

  • La repetibilidad de los casos de prueba es de vital importancia. Si debe escribir una prueba aleatoria, prepárese para documentarla exhaustivamente, porque si falla, deberá saber exactamente por qué.
  • Usar la aleatoriedad como una muleta para la cobertura del código significa que o bien no tiene una buena cobertura o no comprende el dominio lo suficiente como para saber qué constituye un caso de prueba representativo. Averiguar cuál es verdadero y arreglarlo en consecuencia.

En suma, la aleatoriedad a menudo puede ser más problemas de lo que vale la pena. Considere cuidadosamente si va a utilizarlo correctamente antes de apretar el gatillo. Finalmente decidimos que los casos de prueba aleatoria eran una mala idea en general y que debían usarse con moderación, en todo caso.

+0

Uso datos de prueba al azar extensamente. Nunca he tenido una sola situación en la que haya sido más problemático de lo que valía. Mis pruebas aleatorias son tan simples que siempre puedo decir exactamente por qué fallan. He * tenido * pruebas aleatorias que revelan suposiciones erróneas en mi código. Los casos de prueba aleatorios son una idea mucho mejor que los códigos rígidos y deben usarse siempre que sea posible. Nunca codifique sus datos de prueba si puede evitarlos, eso es hacer trampa en el solitario. –

+0

Además, no es necesario que necesite un generador de números aleatorios repetible. Un vuelco del valor en el caso de prueba fallado funciona igual de bien. –

13

No me sorprende nadie en este tema o en el Jason Baker linked to mencionado Monte Carlo Testing. Esa es la única vez que he utilizado ampliamente las entradas de prueba aleatorias. Sin embargo, era muy importante hacer la prueba reproducible, al tener una semilla constante para el generador de números aleatorios para cada caso de prueba.

+0

+1 para el comentario reproducible. Controlar el estado inicial del generador aleatorio es muy importante. Si encuentras un comportamiento extraño, vas a querer intentarlo de nuevo. –

+0

otro +1 para reproducible. – peterchen

+0

Thirded. Cuando uso pruebas aleatorias, siempre agrego una forma de informar y establecer la semilla. Aunque en general trato de evitar depender de la aleatorización ... – Jason

2

Ya se ha publicado mucha buena información, pero ver también: Fuzz Testing. Se dice en la calle que Microsoft usa este enfoque en muchos de sus proyectos.

+0

Me alegra que alguien haya mencionado esto. Fuzz Testing es muy útil, pero tenga en cuenta que las pruebas aleatorias deben * además * a las pruebas repetibles. – vasi

+0

@vasi Si "aleatorio" incluye pseudoaleatorio, entonces no entra en conflicto con la repetibilidad. ¿Qué hay de iniciar sesión la semilla? –

1

Mi experiencia con las pruebas es principalmente con programas simples escritos en C/Python/Java, así que no estoy seguro si esto es totalmente aplicable, pero cada vez que tengo un programa que puede aceptar cualquier tipo de entrada del usuario, siempre Incluya una prueba con datos de entrada aleatorios, o al menos datos de entrada generados por la computadora de una manera impredecible, porque nunca puede hacer suposiciones sobre lo que los usuarios ingresarán. O, bueno, puede, pero si lo hace, entonces algún pirata informático que no hace esa suposición puede encontrar un error que ha pasado por alto por completo. La entrada generada por la máquina es la mejor (¿la única?) Forma que conozco para mantener el sesgo humano completamente fuera de los procedimientos de prueba. Por supuesto, para reproducir una prueba fallida, debe hacer algo como guardar la entrada de prueba en un archivo o imprimirla (si es texto) antes de ejecutar la prueba.

1

Las pruebas aleatorias son una mala práctica siempre que no tenga una solución para el problema de oráculo, es decir, determine cuál es el resultado esperado de su software dada su entrada.

Si resolvió el problema oráculo, puede ir un paso más allá de la generación de entradas aleatorias simples. Puede elegir distribuciones de entrada para que las partes específicas de su software se ejerciten más que con simple al azar.

A continuación, cambia de las pruebas aleatorias a las pruebas estadísticas.

if (a > 0) 
    // Do Foo 
else (if b < 0) 
    // Do Bar 
else 
    // Do Foobar 

Si selecciona a y b al azar en int gama, hace ejercicio Foo 50% de las veces, Bar 25% del tiempo y Foobar 25% del tiempo. Es probable que encuentre más errores en Foo que en Bar o Foobar.

Si selecciona a tal que es negativo 66.66% del tiempo, Bar y Foobar ejercen más que con su primera distribución.De hecho, las tres ramas se ejercitan cada 33.33% del tiempo.

Por supuesto, si el resultado observado es diferente al resultado esperado, debe registrar todo lo que pueda ser útil para reproducir el error.

+0

No necesita pruebas estadísticas para esto, solo una relación mensurable entre su entrada y su salida. –

1

Yo sugeriría echar un vistazo a maquinista:

http://github.com/notahat/machinist/tree/master

maquinista va a generar datos para usted, pero es repetible, por lo que cada ejecución de prueba tiene los mismos datos al azar .

Puede hacer algo similar sembrando el generador de números aleatorios consistentemente.

+0

¿Necesita tener ActiveRecord/Rails para poder utilizar el maquinista? –

+0

Creo que depende de ActiveRecord, pero puede usarlo fuera de Rails. –

0

La efectividad de estas pruebas depende en gran medida de la calidad del generador de números aleatorios que utilice y de cuán correcto es el código que traduce la salida de RNG en datos de prueba.

Si el RNG nunca produce valores que causen que su código entre en alguna condición de borde, no tendrá este caso cubierto. Si su código que traduce la salida del RNG a la entrada del código que prueba es defectuoso, puede ocurrir que incluso con un buen generador, no llegue a todos los casos extremos.

¿Cómo lo comprobará?

0

El problema con la aleatoriedad en los casos de prueba es que la salida es, bueno, aleatoria.

La idea detrás de las pruebas (especialmente las pruebas de regresión) es comprobar que no haya nada roto.

Si encuentra algo que está roto, debe incluir esa prueba cada vez a partir de ese momento, de lo contrario no tendrá un conjunto constante de pruebas. Además, si ejecuta una prueba aleatoria que funciona, entonces debe incluir esa prueba, porque es posible que pueda romper el código para que la prueba falle.

En otras palabras, si tiene una prueba que utiliza datos aleatorios generados sobre la marcha, creo que esta es una mala idea. Sin embargo, si utilizas un conjunto de datos aleatorios, QUE LUEGO ALMACENAR Y REUTILIZAR, esta puede ser una buena idea. Esto podría tomar la forma de un conjunto de semillas para un generador de números aleatorios.

Este almacenamiento de los datos generados le permite encontrar la respuesta 'correcta' a estos datos.

lo tanto, yo recomendaría el uso de datos al azar para explorar su sistema, pero el uso de datos definidos en sus pruebas (datos que pueden haber sido originalmente generados al azar)

0

El uso de los datos de prueba al azar es una práctica excelente - duro los datos de prueba codificados solo prueban los casos que explícitamente pensaste, mientras que los datos aleatorios eliminan tus suposiciones implícitas que podrían estar equivocadas.

Recomiendo usar Factory Girl y ffaker para esto. (Nunca use los accesorios Rails para nada bajo ninguna circunstancia.)

Cuestiones relacionadas