Search code examples
ruby-on-railsrspeccapybarasidekiq

Rspec feature test with sidekiq mail delivery, how to wait for mail observer?


when I send a mail through deliver_later it is managed by sidekiq, and then my registered mail observer is triggered.

I have a Capybara test that checks states changed inside observer code, but it fails randomly if observer is not executed right after the clicks, and the expectation doesn't work correctly.

Example:

# spec
scenario 'Test that fails randomly' do
  click_link "Go!"
  # MyModel#done is a boolean attribute, so we have #done? method availiable
  expect(MyModel.first.done?).to be true
end

# The controller that manages the Go! link, triggers a mailer.
# After the mailer, this is executed.
# Registered observer
def delivered_mail(mail)
  email = Email.find_by_message_id mail.message_id
  email.user.update_attributes done: true
end

Fun fact: If I execute this scenario isolated, the test will always pass. If I execute the test suite completely, the test will 9:1 fail:pass more or less. ¯_(ツ)_/¯

Tried putting this in rails_helper:

require 'sidekiq/testing'
RSpec.configure do |config|
  Sidekiq::Testing.inline!
end

And also putting Sidekiq::Testing.inline! in the very first line of the scenario block... nothing. The same fun fact.

Update:

Added database_cleaner gem, and now it fails everytime.


Solution

  • Actions in Capybara (click_link, etc) know nothing about any behaviors they trigger. Because of this there is no guarantee as to what the app will have done after your click_link line returns, other than the link will have been clicked, and the browser will have started to perform whatever that action triggers. Your test then immediately checks 'MyModel.first.done?` while the browser could still be submitting a request (This is one reason why directly checking database records in feature tests is generally frowned upon).

    The solution to that (and end up with tests that will work across multiple drivers reliably is to check for a visual change on the page that indicates the action has completed. You also need to set up ActiveJob properly for testing so you can make sure the jobs are executed. To do this you will need to include ActiveJob::TestHelper, which can be done in your RSpec config or individual scenarios, and you will need to make sure ActiveJob::Base.queue_adapter = :test is set (can be done in config/environment/tests.rb file if wanted). Then assuming your app shows a message "Mail sent!" on screen when the action has completed you would do

    include ActiveJob::TestHelper 
    ActiveJob::Base.queue_adapater = :test
    ...
    perform_enqueued_jobs do
      click_link "Go!"
      expect(page).to have_text('Mail sent!') # This will wait for the message to appear, which guarantees the action has completed and enqueued the job
    end # when this returns any jobs enqueued during the block will have been executed
    
    expect(MyModel.first.done?).to be true