Как правильно проверить метод retry_on ActiveJob с помощью rspec?

В последние несколько дней я безуспешно пытался протестировать этот метод.

Еще одна вещь, которую я хотел бы сделать - это rescue ошибка, которая всплывает после последней попытки повторения.

Пожалуйста, смотрите мои комментарии и фрагменты кода ниже.

Исходный код для retry_on здесь также для контекста.

Вот пример кода и тесты:

   my_job.rb

   retry_on Exception, wait: 2.hours, attempts: 3 do |job, exception|
   # some kind of rescue here after job.exceptions == 3  
   # then notify Bugsnag of failed final attempt.
   end

   def perform(an_object)
     an_object.does_something
   end

   my_spec.rb
   it 'receives retry_on 3 times' do
     perform_enqueued_jobs do
       expect(AnObject).to receive(:does_something).and_raise { Exception }.exactly(3).times
       expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts: 3).exactly(3).times
       MyJob.perform_later(an_object)
     end
     assert_performed_jobs 3
   end

Ответ теста на неудачу:

      1) MyJob.perform receives retry_on 3 times
         Failure/Error: expect(job).to receive(:retry_on).with(wait: 4.hours, attempts: 3).exactly(3).times

   (MyJob (class)).retry_on({:wait=>2 hours, :attempts=>3})
       expected: 3 times with arguments: ({:wait=>2 hours, :attempts=>3})
       received: 0 times
 # ./spec/jobs/my_job_spec.rb:38:in 'block (4 levels) in <top (required)>'
 # ./spec/rails_helper.rb:48:in 'block (3 levels) in <top (required)>'
 # ./spec/rails_helper.rb:47:in 'block (2 levels) in <top (required)>'

Я также пытался сделать работу двойной и заглушить метод retry_on, и это тоже не работает.

Я также попытался использовать Timecop для ускорения пересылки времени ожидания, и тесты по-прежнему не выполняются:

           my_spec.rb
   it 'receives retry_on 3 times' do
     perform_enqueued_jobs do
       expect(AnObject).to receive(:does_something).and_raise { Exception }.exactly(3).times
       Timecop.freeze(Time.now + 8.hours) do
         expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts: 3).exactly(3).times
       end
       MyJob.perform_later(an_object)
     end
     assert_performed_jobs 3
   end

Это метод класса ActiveJob, и я подтвердил это в терминале byebug, что это так и есть с моим классом работы.

Разве этот тест не должен работать? Он ожидает, что класс получит метод класса с определенными аргументами. Мой byebug также получает удар, когда я помещаю его в блок retry_on, поэтому я знаю, что метод вызывается несколько раз.

Это почти как если бы он был вызван в другом классе, что очень запутанно, и я не думаю, что это так, но я в конце своей веревки с этим.

Я почти решил эту проблему, отделив свои тесты от тестирования самой логики рельсов retry_on до тестирования моей бизнес-логики вокруг нее. Этот способ также лучше в случае, когда рельсы когда-либо изменяют логику retry_on.

ОДНАКО, это НЕ работает для более чем одного теста. Если вы используете это более чем в одном случае, последний тест будет прерван и он скажет, что выполнил больше заданий, чем ожидалось.

 my_spec.rb
 it 'receives retry_on 3 times' do
   perform_enqueued_jobs do
     allow(AnObject).to receive(:does_something).and_raise { Exception }
     expect(AnObject).to receive(:does_something).exactly(3).times
     expect(Bugsnag).to receive(:notify).with(Exception).once
     MyJob.perform_later(an_object)
   end
   assert_performed_jobs 3
 end

my_job.rb

retry_on Exception, wait: , attempts: 3 do |job, exception|
  Bugsnag.notify(exception)
end

def perform(an_object)
  an_object.does_something
end

Любая помощь/понимание этого будет принята с благодарностью.

Также хотелось бы получить рекомендацию о том, как обрабатывать всплывающее исключение после максимальных попыток. Я подумываю о том, чтобы вызвать ошибку в блоке retry_on, а затем вызвать discard_on триггер для возникшей ошибки.

Спасибо, замечательное сообщество Qaru!

Ответы

Ответ 1

Это формат спецификаций, необходимых для retry_on который, наконец, работал для меня:

it 'receives retry_on 10 times' do
  allow_any_instance_of(MyJob).to receive(:perform).and_raise(MyError.new(nil))
  allow_any_instance_of(MyJob).to receive(:executions).and_return(10)
  expect(Bugsnag).to receive(:notify)
  MyJob.perform_now(an_object)
end

it 'handles error' do
  allow_any_instance_of(MyJob).to receive(:perform).and_raise(MyError.new(nil))
  expect_any_instance_of(MyJob).to receive(:retry_job)
  perform_enqueued_jobs do
    MyJob.perform_later(an_object)
  end
end

В первом случае executions - это метод ActiveJob, который запускается, устанавливается и проверяется каждый раз, retry_on выполняется retry_on. Мы издеваемся над ним, чтобы вернуть 10, а затем ожидаем, что он позвонит в Bugsnag. retry_on вызывает только то, что вы дали ему в блоке, как только все attempts были выполнены. Так что это работает.

Для второго случая, Затем mock ошибка для повышения для экземпляра задания. Затем мы проверяем правильность приема retry_job (который вызывает retry_on под капотом), чтобы подтвердить, что он делает правильные вещи. Затем мы perform_later вызов minitest блоке minitest perform_enqueued_jobs и называем его днем.

Ответ 2

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

RSpec.describe MyJob, type: :job do
  include ActiveJob::TestHelper

  context 'when 'MyError' is raised' do
    before do
      allow_any_instance_of(described_class).to receive(:perform).and_raise(MyError.new)
    end

    it 'makes 4 attempts' do
      assert_performed_jobs 4 do
        described_class.perform_later rescue nil
      end
    end

    it 'does something in the 'retry_on' block' do
      expect(Something).to receive(:something)

      perform_enqueued_jobs do
        described_class.perform_later rescue nil
      end
    end
  end
end

Обратите внимание, что rescue nil (или какая-либо другая форма спасения) требуется, если вы позволили исключениям всплыть в конце.

Обратите внимание, что perform_now не считается "заданием в очереди". Таким образом, выполнение described_class.perform_now приводит к одной меньшей попытке, подсчитанной assert_performed_jobs.

Ответ 3

В первой спецификации

expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts:3).exactly(3).times

Это никогда не будет работать, поскольку метод класса retry_on будет вызываться на этапе инициализации класса, то есть при загрузке этого класса в память, а не при выполнении спецификации

Во втором spec вы пытались заставить его работать с использованием timecop, но все же не удалось по той же причине

Третья спецификация относительно более реалистична, но

assert_performed_jobs 3

не будет работать без прохождения блока

Что-то вроде

assert_performed_jobs 2 do
  //call jobs from here
end

Ответ 4

ИМХО, вы должны оставить тестирование ActiveJob с командой rails.

Вам нужно только убедиться, что вы правильно настраиваете задание:

it 'retries the job 10 times with 2 minutes intervals' do
  allow(MyJob).to receive(:retry_on)
  load 'app/path/to/job/my_job.rb'
  expect(MyJob).to have_received(:retry_on)
    .with(
      Exception,
      wait: 2.minutes,
      attempts: 10
    )
end