Pregunta

Desde que comencé a usar rspec, he tenido un problema con la noción de accesorios. Mis principales preocupaciones son las siguientes:

  1. Utilizo las pruebas para revelar un comportamiento sorprendente. No siempre soy lo suficientemente inteligente como para enumerar todos los casos límite posibles para los ejemplos que estoy probando. El uso de accesorios codificados parece limitante 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 fijación 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
    

    El uso del primer método no le da al lector ninguna indicación de cuál es el artículo más caro, solo que su precio es 100. Los tres métodos le piden al lector que confíe en que el accesorio : costoso es el más caro listado 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 se trata de un archivo grande, podría llevar mucho tiempo descubrir cuál es el problema.

Una cosa que comencé a hacer es agregar 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 hago esto en realidad son un poco más limpios. Tengo una clase que maneja la generación y limpieza de todos los modelos, pero este código es lo suficientemente claro para mi ejemplo). Entonces, en el ejemplo anterior, yo podría probar de la siguiente manera. Una advertencia para la finta de corazón: mi código depende en gran medida del 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 mejor un comportamiento sorprendente. Cuando genero datos de esta manera, de vez en cuando me encuentro con un caso de borde en el que mi código no se comporta como se esperaba, pero que no habría captado si solo estuviera utilizando dispositivos fijos. 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 fallaba en el primer should . Ver las fallas no deterministas en AutoSpec me daría una idea de que algo andaba mal. Si solo estuviera usando accesorios, podría llevar mucho más tiempo descubrir un error de este tipo.

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 artículos 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? ¿Escribir un método generate_random para cada modelo es demasiado trabajo? ¿O esto funciona?

¿Fue útil?

Solución

Esta es una respuesta a su segundo punto:

  

(2) Utilizo las pruebas como una forma de documentación para el código. Si tengo valores de fijación 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 entendibles por sí mismos. El uso de accesorios es problemático, ya que divide las condiciones previas del ejemplo de sus resultados esperados.

Debido a esto, muchos usuarios de RSpec han dejado de usar accesorios por completo. En su lugar, construya los objetos necesarios en el ejemplo de especificaciones en sí.

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 termina con un montón de código repetitivo para la creación de objetos, debe echar un vistazo a algunas de las muchas bibliotecas de fábrica de objetos de prueba, como factory_girl , Machinist , o < a href = "http://replacefixtures.rubyforge.org/" rel = "nofollow noreferrer"> Reemplazo de accesorios .

Otros consejos

No me sorprende que nadie en este tema o en el Jason Baker esté vinculado a mencionado Pruebas de Monte Carlo . Esa es la única vez que he usado ampliamente entradas de prueba aleatorias. Sin embargo, era muy importante hacer que la prueba fuera reproducible, teniendo una semilla constante para el generador de números aleatorios para cada caso de prueba.

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

  • La repetibilidad de los casos de prueba es de suma importancia. Si debe escribir una prueba aleatoria, prepárese para documentarla ampliamente, porque si / cuando falla, necesitará saber exactamente por qué.
  • Usar la aleatoriedad como una muleta para la cobertura del código significa que no tienes una buena cobertura o no entiendes el dominio lo suficiente como para saber qué constituyen los casos de prueba representativos. Averigua cuál es la verdad y arréglalo en consecuencia.

En resumen, la aleatoriedad a menudo puede ser más problemática de lo que vale. Considere cuidadosamente si lo va a usar correctamente antes de apretar el gatillo. En última instancia, decidimos que los casos de prueba aleatorios eran una mala idea en general y que se utilizarían con moderación, si es que se usaban.

Ya se ha publicado mucha información buena, pero vea también: Prueba de fuzz . Se dice que Microsoft usa este enfoque en muchos de sus proyectos.

Mi experiencia con las pruebas es principalmente con programas simples escritos en C / Python / Java, por lo que no estoy seguro de si esto es completamente aplicable, pero siempre que tengo un programa que puede aceptar cualquier tipo de entrada del usuario, siempre incluyo una prueba con datos de entrada aleatorios, o al menos datos de entrada generados por la computadora de manera impredecible, porque nunca puede hacer suposiciones sobre lo que ingresarán los usuarios. O bien, usted puede , pero si lo hace, algún hacker que no haga esa suposición puede encontrar un error que usted pasó por alto por completo. La entrada generada por la máquina es la mejor (¿ú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.

Las pruebas aleatorias son una mala práctica siempre y cuando no tenga una solución para el problema de Oracle , es decir, determinar cuál es el resultado esperado de su software dada su entrada.

Si resolvió el problema de Oracle, puede ir un paso más allá que la simple generación de entrada aleatoria. Puede elegir distribuciones de entrada para que partes específicas de su software se ejerzan más que con un simple azar.

Luego cambia de pruebas aleatorias a pruebas estadísticas.

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

Si selecciona a y b aleatoriamente en el rango int , ejercita Foo el 50% del tiempo , Bar el 25% del tiempo y Foobar el 25% del tiempo. Es probable que encuentre más errores en Foo que en Bar o Foobar .

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

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

Sugeriría echar un vistazo a Machinist:

  

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

Machinist generará datos para usted, pero es repetible, por lo que cada ejecución de prueba tiene los mismos datos aleatorios.

Podría hacer algo similar al sembrar el generador de números aleatorios de manera consistente.

Un problema con los casos de prueba generados aleatoriamente es que la validación de la respuesta debe calcularse por código y no puede estar seguro de que no tenga errores :)

La efectividad de tales pruebas depende en gran medida de la calidad del generador de números aleatorios que usa y de qué tan 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 una condición de caso límite, no tendrá este caso cubierto. Si su código que traduce la salida del RNG en la entrada del código que prueba es defectuoso, puede suceder que incluso con un buen generador aún no llegue a todos los casos límite.

¿Cómo probarás eso?

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 verificar que nada esté roto.

Si encuentra algo que no funciona, debe incluir esa prueba cada vez que lo haga, de lo contrario no tendrá un conjunto consistente 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 es una mala idea. Sin embargo, si utiliza un conjunto de datos aleatorios, QUE ALMACENAR Y REUTILIZAR, 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.

Por lo tanto, recomendaría usar datos aleatorios para explorar su sistema, pero use datos definidos en sus pruebas (que pueden haber sido originalmente generados aleatoriamente)

El uso de datos de prueba aleatorios es una práctica excelente: los datos de prueba codificados solo prueban los casos en los que pensó explícitamente, mientras que los datos aleatorios eliminan sus suposiciones implícitas que podrían estar equivocadas.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top