2012-06-02 29 views
5

Estoy tratando de mantener mis especificaciones limpias y SECAS, pero tengo algunas pruebas para una API que son idénticas, excepto por la versión de API que se está probando. Podría repetir las especificaciones simplemente usando algo como esto:Repitiendo grupos de ejemplos de RSpec con diferentes argumentos

%w(v1 v2).each do |version| 
    describe "Query #{version} API" do 
    it "responds with JSON" 
     # make the call using the version 
    end 
    end 
end 

pero me gustaría algo un poco más limpia, y por lo que he escrito este método:

module RepetitivelyDescribe 
    def repetitively_describe(*args, &example_group_block) 
    options = args.extract_options! 
    options.delete(:for).each do |item| 
     item_args = args.collect(&:dup) + [options.dup] 
     item_args[0] << " [#{item}]" 

     describe(*item_args) do 
     example_group_block.call item 
     end 
    end 
    end 
end 

RSpec::Core::ExampleGroup.extend RepetitivelyDescribe 

Y entonces mi prueba podría mirar de la misma familia:

repetitively_describe "Query API", :for => %(v1 v2) do |version| 
    it "responds with JSON" 
    # make the call using the version 
    end 
end 

comprendo que esto es un poco de pedantería, pero es uno menos nivel de sangría, y si voy a estar haciendo esta llamada, un montón, me gustaría tenerlo limpiador.

Pero, por supuesto, no funciona del todo como me gustaría. La llamada al describe dentro de mi repetitively_describe no se registra en la salida de RSpec (cuando se usa el resultado del formato de documentación), aunque los ejemplos dentro de sí se repiten y usan el argumento de bloque de versión como se esperaba. Básicamente, ese nivel de contexto se pierde (se guardan los bloques describe dentro y fuera del bloque repetitively_describe).

Hay un código de ejemplo más detallado en a gist en caso de ser necesario. Alguna pista sobre por qué esto no está funcionando del todo bien?

+0

Usaría personalmente un contexto compartido, o un grupo de ejemplo compartido para esto. 'it_behaves_like" una API de consulta "do let (: version) {: v1} end'. https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples https://www.relishapp.com/rspec/rspec-core/v/2-9/docs/example -groups/shared-context – d11wtq

Respuesta

5

Así (disculpa si repito cosas que ya sabes) pero cada vez que llamas describe/contexto rspec crea una nueva clase que es una subclase del grupo de ejemplo actual (que finalmente es una subclase de RSpec::Core::ExampleGroup) y luego usa module_eval para evaluar el bloque en el contexto de esa clase. Si me quedo

describe "foo" do 
    puts "#{self}; #{self.superclass}" 
    describe "behaviour 1" do 
    puts "#{self}; #{self.superclass}" 
    context "with x" do 
     puts "#{self}; #{self.superclass}" 
    end 
    end 
end 

entonces la salida es

#<Class:0x007fb772bfbc70>; RSpec::Core::ExampleGroup 
#<Class:0x007fb772bfb180>; #<Class:0x007fb772bfbc70> 
#<Class:0x007fb772bfa5f0>; #<Class:0x007fb772bfb180> 

Cuando se llama a it rspec crear un objeto Example y lo añade a una variable de instancia de clase en sí (el grupo de ejemplo actual). rspec también pega el grupo de ejemplo actual en los metadatos del ejemplo, caminar por este árbol de grupos de ejemplo es lo que le da la descripción completa del ejemplo.

Su método repetitively_describe llama a describe, por lo que en el momento en que llama al example_group_block.call item es realmente el grupo de ejemplo recién creado. Cuando se evalúa el proceso, por supuesto, recuerda cuál fue el valor de self cuando se llamó para que sus llamadas a it se hagan al grupo de ejemplo que estaba actual cuando repeatitively_describe (fácilmente verificable rociando algunas llamadas para verificar el valor de self en todo tu codigo). De forma similar, una llamada para describir agrega el grupo de ejemplo como hijo del grupo de ejemplo externo, no el creado por repetitively_describe.

Lo que por supuesto tiene que hacer es llamar al example_group_block conservando el valor correcto de uno mismo.

module RepetitivelyDescribe 
    def repetitively_describe(*args, &example_group_block) 
    options = args.extract_options! 
    options.delete(:for).each do |item| 
     item_args = args.collect(&:dup) + [options.dup] 
     item_args[0] << " [#{item}]" 

     describe(*item_args) do 
     class_exec(item, &example_group_block) 
     end 
    end 
    end 
end 

con este cambio

describe('foo') do 
    repetitively_describe "Query API", :for => %w(v1 v2) do |version| 
    it "responds with JSON" 
    end 
end.descendant_filtered_examples.collect(&:full_description) 

salidas ["foo Query API [v1] responds with JSON", "foo Query API [v2] responds with JSON"] en lugar de ["foo responds with JSON", "foo responds with JSON"] antes del cambio.

+0

Perfecto, gracias, ¡y aprecio todos los detalles también! – pat

+0

En realidad, no del todo perfecto: los métodos definidos dentro del bloque 'repetitively_describe' no están disponibles para los ejemplos internos. ¿Alguna idea? – pat

+1

Ah sí, quiere class_exec en lugar de instance_exec en ese caso - cambios donde los métodos se definen cuando se hace 'def blah; end' dentro del bloque –

Cuestiones relacionadas