Динамическая генерация спецификации RSpec

Я хотел бы генерировать серию спецификаций динамически на основе внешнего источника данных. В частности, у меня есть электронная таблица Google, которая загружается, и каждая строка в электронной таблице должна использоваться для собственного тестового примера:

describe "Cases" do
  before(:all) do
    # Download spreadsheet and
    # populate cases in DB
  end

  Cases.each do |case|
    it "Case #{case.num}" do
      # spec
    end
  end
end

Это не работает, потому что для начинающих с RSpec нет (как я понимаю) "видеть" любые спецификации во время компиляции, поэтому before(:all) никогда не выполняется. Если бы я поместил пустой блок it, он бы выполнил before(:all), но затем я столкнулся с проблемой, которую Cases.each оценивает раньше всего, что пусто, потому что оно не было еще не заполнен блоком before(:all).

Короче говоря, я в замешательстве, и мое понимание RSpec кажется очень ограниченным. Я хотел бы получить данные, использовать эти данные, чтобы настроить набор спецификаций, а затем запустить их с помощью RSpec. Это могло бы (возможно?) Работать, если Cases были массивом, настроенным заранее (за пределами блока описания), но мне нужно, чтобы он был настроен во время выполнения. Это то, что я хочу сделать в RSpec?

Ответ 1

Это не работает, потому что... RSpec не... "видеть" любые спецификации на время компиляции...

Мне было интересно, почему и какое-то исследование. Если вы изучите следующий код и вывод, вы можете сделать некоторые выводы:

  • в Ruby отсутствует время компиляции
  • тело класса/модуля немедленно оценивается и выполняется
  • Таким образом, метод describe
  • тело блока describe немедленно обрабатывается RSpec
  • RSpec хранит блоки before и it для последующего выполнения
  • когда разбор файла завершается, RSpec начинает сообщать describe и выполнение сохраненных блоков before/it

Следующий код демонстрирует это.

module SO
    puts '>>>>> Ruby sees module SO and executes/evaluates its body'
    cases = [1,2,3]

    describe "SO Cases" do
        puts "in module SO, RSpec sees describe Cases self=#{self}"
        before(:each) do
            puts '  in before(:all)'
        end

        cases.each do |case_|
            puts "    in each loop with case_=#{case_}"
            it "Case #{case_}" do
                puts "        check spec for case #{case_}"
            end
        end
    end
end

module M # to avoid "warning: class variable access from toplevel"
    puts '>>>>> Ruby sees module M and executes/evaluates its body'
    describe "M Cases" do
        puts "in module M, RSpec sees describe Cases self=#{self}, ancestors :"
        ancestors.each {|a| puts "    #{a}"}
        print 'self.methods.grep(/^it/) : '; p self.methods.grep(/^it/).sort
        before(:all) do
            puts "  in before(:all) self=#{self}"
            @@cases = [1,2,3]
            puts "  ... now cases=#{@@cases}"
            File.open('generated_cases.rb', 'w') do |fgen|
                fgen.puts 'puts "\n*** inside generated_cases.rb ***"'
                fgen.puts 'module GenSpecs'
                fgen.puts "puts '>>>>> Ruby sees module GenSpecs and executes/evaluates its body'"
                fgen.puts '    describe "GenSpecs Cases" do'
                fgen.puts '        puts "in module GenSpecs, RSpec sees describe Cases"'
                @@cases.each do |case_|
                    puts "    in each loop with case_=#{case_}"
                    fgen.puts <<-IT_SPECS
        it "Case #{case_}" do
            puts "    some spec for case_=#{case_}"
        end
        IT_SPECS
                end
                fgen.puts '    end'
                fgen.puts 'end # module GenSpecs'
                fgen.puts "puts '>>>>> end of ruby file generated_cases.rb <<<<<'"
            end
            puts 'file generated_cases.rb has been closed'
            require_relative 'generated_cases'
        end

        it 'has loaded Cases' do
            @@cases.should_not be_empty
        end
    end
end
puts '>>>>> end of ruby file t2_spec.rb <<<<<'

Исполнение:

$ ruby -v
ruby 1.9.2p320 (2012-04-20 revision 35421) [x86_64-darwin12.2.0]
$ rspec --version
2.12.2
$ rspec --format nested t2_spec.rb 
>>>>> Ruby sees module SO and executes/evaluates its body
in module SO, RSpec sees describe Cases self=#<Class:0x007fcaf49a6e80>
    in each loop with case_=1
    in each loop with case_=2
    in each loop with case_=3
>>>>> Ruby sees module M and executes/evaluates its body
in module M, RSpec sees describe Cases self=#<Class:0x007fcaf2852e28>, ancestors :
    #<Class:0x007fcaf2852e28>
    RSpec::Core::ExampleGroup
    ...
self.methods.grep(/^it/) : [:it, :it_behaves_like, :it_should_behave_like, :its]
>>>>> end of ruby file t2_spec.rb <<<<<

SO Cases
  in before(:all)
        check spec for case 1
  Case 1
  in before(:all)
        check spec for case 2
  Case 2
  in before(:all)
        check spec for case 3
  Case 3

M Cases
  in before(:all) self=#<RSpec::Core::ExampleGroup::Nested_2:0x007fcaf2836ca0>
  ... now cases=[1, 2, 3]
    in each loop with case_=1
    in each loop with case_=2
    in each loop with case_=3
file generated_cases.rb has been closed

*** inside generated_cases.rb ***
>>>>> Ruby sees module GenSpecs and executes/evaluates its body
in module GenSpecs, RSpec sees describe Cases
>>>>> end of ruby file generated_cases.rb <<<<<
  has loaded Cases

GenSpecs Cases
    some spec for case_=1
  Case 1
    some spec for case_=2
  Case 2
    some spec for case_=3
  Case 3

Finished in 0.01699 seconds
7 examples, 0 failures

Создание файла и его требование для демонстрации и могут не работать в вашем случае. Я бы рекомендовал сделать это в два этапа: сначала вы читаете электронную таблицу и создаете файл .rb с примерами describe и несколькими it. На втором этапе вы запускаете Ruby для обработки сгенерированного файла.

Ответ 2

Удалите блок before.

Код в describe выполняется при загрузке rspec. Недостаток заключается в том, что он загрузит и сканирует лист, даже если эти спецификации не будут запущены.

Следующая проблема, с которой вам придется столкнуться, заключается в том, что db получает опорожненную после каждой спецификации. Поэтому вам нужно будет повторно заполнить базу данных в блоке перед блоком для каждой спецификации, созданной ниже.

Я хотел бы подчеркнуть, что я бы не рекомендовал этого, я бы глубоко подумал о том, что тестовые примеры выставляются динамически. Рассмотрите общие тесты, которые доказывают, что ваш код работает без внешних данных. Или вытащите копию электронной таблицы, сохраните ее в spec/assets и загрузите с помощью более простых тестов для этого конкретного примера.

describe "Cases" do
  # Download spreadsheet and
  # populate cases in DB
  before(:each) do
    # repopulate the DB
  end

  Cases.each do |case|
    it "Case #{case.num}" do
      # spec
    end
  end

  # clean out the db so that the first executed test is not polluted
end