Active Job: Correctly use the desired test adapter in tests

This commit is contained in:
Alex Ghiculescu 2023-07-02 20:33:59 +10:00 committed by Alex
parent 25f22503af
commit 2194a738c7
12 changed files with 2225 additions and 1794 deletions

View File

@ -1,3 +1,16 @@
* All tests now respect the `active_job.queue_adapter` config
Previously if you had set `config.active_job.queue_adapter` in your `config/application.rb`
or `config/environments/test.rb` file, the adapter you selected was previously not used consistently
across all tests. In some tests your adapter would be used, but other tests would use the `TestAdapter`.
In Rails 7.2, all tests will respect the `queue_adapter` config if provided. If no config is provided,
the `TestAdapter` will continue to be used.
See [#48585](https://github.com/rails/rails/pull/48585) for more details.
*Alex Ghiculescu*
* Make Active Job transaction aware when used conjointly with Active Record.
A common mistake with Active Job is to enqueue jobs from inside a transaction,

View File

@ -15,7 +15,8 @@ module ActiveJob
# = Active Job Queue adapter
#
# The +ActiveJob::QueueAdapter+ module is used to load the
# correct adapter. The default queue adapter is the +:async+ queue.
# correct adapter. The default queue adapter is +:async+,
# which loads the ActiveJob::QueueAdapters::AsyncAdapter.
module QueueAdapter # :nodoc:
extend ActiveSupport::Concern
@ -24,21 +25,21 @@ module ActiveJob
class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
delegate :queue_adapter, to: :class
self.queue_adapter = :async
end
# Includes the setter method for changing the active queue adapter.
module ClassMethods
# Returns the backend queue provider. The default queue adapter
# is the +:async+ queue. See QueueAdapters for more information.
# is +:async+. See QueueAdapters for more information.
def queue_adapter
self.queue_adapter = :async if _queue_adapter.nil?
_queue_adapter
end
# Returns string denoting the name of the configured queue adapter.
# By default returns <tt>"async"</tt>.
def queue_adapter_name
self.queue_adapter = :async if _queue_adapter_name.nil?
_queue_adapter_name
end

View File

@ -41,7 +41,7 @@ module ActiveJob
initializer "active_job.set_configs" do |app|
options = app.config.active_job
options.queue_adapter ||= :async
options.queue_adapter ||= (Rails.env.test? ? :test : :async)
config.after_initialize do
options.each do |k, v|

View File

@ -39,10 +39,12 @@ module ActiveJob
end
def before_setup # :nodoc:
test_adapter = queue_adapter_for_test
queue_adapter_changed_jobs.each do |klass|
klass.enable_test_adapter(test_adapter)
if (queue_adapter_specific_to_this_test_class = queue_adapter_for_test)
klass.enable_test_adapter(queue_adapter_specific_to_this_test_class)
elsif klass._queue_adapter.nil?
klass.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new)
end
end
clear_enqueued_jobs
@ -61,7 +63,6 @@ module ActiveJob
# Override this method to specify a different adapter. The adapter must
# implement the same interface as ActiveJob::QueueAdapters::TestAdapter.
def queue_adapter_for_test
ActiveJob::QueueAdapters::TestAdapter.new
end
# Asserts that the number of enqueued jobs matches the given number.
@ -118,6 +119,8 @@ module ActiveJob
# end
# end
def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil, &block)
require_active_job_test_adapter!("assert_enqueued_jobs")
if block_given?
original_jobs = enqueued_jobs_with(only: only, except: except, queue: queue)
@ -180,6 +183,8 @@ module ActiveJob
#
# assert_enqueued_jobs 0, &block
def assert_no_enqueued_jobs(only: nil, except: nil, queue: nil, &block)
require_active_job_test_adapter!("assert_no_enqueued_jobs")
assert_enqueued_jobs 0, only: only, except: except, queue: queue, &block
end
@ -270,6 +275,8 @@ module ActiveJob
# end
# end
def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block)
require_active_job_test_adapter!("assert_performed_jobs")
if block_given?
original_count = performed_jobs.size
@ -338,6 +345,8 @@ module ActiveJob
#
# assert_performed_jobs 0, &block
def assert_no_performed_jobs(only: nil, except: nil, queue: nil, &block)
require_active_job_test_adapter!("assert_no_performed_jobs")
assert_performed_jobs 0, only: only, except: except, queue: queue, &block
end
@ -394,6 +403,8 @@ module ActiveJob
# end
# end
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, priority: nil, &block)
require_active_job_test_adapter!("assert_enqueued_with")
expected = { job: job, args: args, at: at, queue: queue, priority: priority }.compact
expected_args = prepare_args_for_assertion(expected)
potential_matches = []
@ -496,6 +507,8 @@ module ActiveJob
# end
# end
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, priority: nil, &block)
require_active_job_test_adapter!("assert_performed_with")
expected = { job: job, args: args, at: at, queue: queue, priority: priority }.compact
expected_args = prepare_args_for_assertion(expected)
potential_matches = []
@ -604,7 +617,10 @@ module ActiveJob
# If queue_adapter_for_test is overridden to return a different adapter,
# +perform_enqueued_jobs+ will merely execute the block.
def perform_enqueued_jobs(only: nil, except: nil, queue: nil, at: nil, &block)
return flush_enqueued_jobs(only: only, except: except, queue: queue, at: at) unless block_given?
unless block_given?
require_active_job_test_adapter!("perform_enqueued_jobs (without a block)")
return flush_enqueued_jobs(only: only, except: except, queue: queue, at: at)
end
return _assert_nothing_raised_or_warn("perform_enqueued_jobs", &block) unless using_test_adapter?
@ -646,6 +662,12 @@ module ActiveJob
end
private
def require_active_job_test_adapter!(method)
unless using_test_adapter?
raise ArgumentError.new("#{method} requires the Active Job test adapter, you're using #{queue_adapter.class.name}.")
end
end
def using_test_adapter?
queue_adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter)
end

View File

@ -23,18 +23,20 @@ class InstrumentationTest < ActiveSupport::TestCase
assert_equal 1, events.size
end
test "retry emits an enqueue retry event" do
events = subscribed("enqueue_retry.active_job") do
perform_enqueued_jobs { RetryJob.perform_later("DefaultsError", 2) }
unless adapter_is?(:inline, :sneakers)
test "retry emits an enqueue retry event" do
events = subscribed("enqueue_retry.active_job") do
perform_enqueued_jobs { RetryJob.perform_later("DefaultsError", 2) }
end
assert_equal 1, events.size
end
assert_equal 1, events.size
end
test "retry exhaustion emits a retry_stopped event" do
events = subscribed("retry_stopped.active_job") do
perform_enqueued_jobs { RetryJob.perform_later("CustomCatchError", 6) }
test "retry exhaustion emits a retry_stopped event" do
events = subscribed("retry_stopped.active_job") do
perform_enqueued_jobs { RetryJob.perform_later("CustomCatchError", 6) }
end
assert_equal 1, events.size
end
assert_equal 1, events.size
end
test "discard emits a discard event" do

View File

@ -213,14 +213,14 @@ class LoggingTest < ActiveSupport::TestCase
end
end
def test_enqueue_at_job_logging
events = subscribed { HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
assert_equal(1, events.count)
key, * = events.first
assert_equal("enqueue_at.active_job", key)
rescue NotImplementedError
skip
unless adapter_is?(:inline, :sneakers)
def test_enqueue_at_job_logging
events = subscribed { HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
assert_equal(1, events.count)
key, * = events.first
assert_equal("enqueue_at.active_job", key)
end
end
def test_enqueue_at_job_log_error_when_callback_chain_is_halted
@ -244,14 +244,14 @@ class LoggingTest < ActiveSupport::TestCase
assert_equal("enqueue_at.active_job", key)
end
def test_enqueue_in_job_logging
events = subscribed { HelloJob.set(wait: 2.seconds).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
assert_equal(1, events.count)
key, * = events.first
assert_equal("enqueue_at.active_job", key)
rescue NotImplementedError
skip
unless adapter_is?(:inline, :sneakers)
def test_enqueue_in_job_logging
events = subscribed { HelloJob.set(wait: 2.seconds).perform_later "Cristian" }
assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
assert_equal(1, events.count)
key, * = events.first
assert_equal("enqueue_at.active_job", key)
end
end
def test_enqueue_log_when_enqueue_error_is_set
@ -290,10 +290,12 @@ class LoggingTest < ActiveSupport::TestCase
assert_no_match(/Error performing RescueJob \(Job ID: .*?\) from .*? in .*ms: ArgumentError \(Hair too good\):\n.*\brescue_job\.rb:\d+:in .*perform'/, @logger.messages)
end
def test_enqueue_retry_logging
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 2
assert_match(/Retrying RetryJob \(Job ID: .*?\) after \d+ attempts in 3 seconds, due to a DefaultsError.*\./, @logger.messages)
unless adapter_is?(:inline, :sneakers)
def test_enqueue_retry_logging
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 2
assert_match(/Retrying RetryJob \(Job ID: .*?\) after \d+ attempts in 3 seconds, due to a DefaultsError.*\./, @logger.messages)
end
end
end
@ -302,18 +304,20 @@ class LoggingTest < ActiveSupport::TestCase
assert_match(/Retrying RescueJob \(Job ID: .*?\) after \d+ attempts in 0 seconds\./, @logger.messages)
end
def test_retry_stopped_logging
perform_enqueued_jobs do
RetryJob.perform_later "CustomCatchError", 6
unless adapter_is?(:inline, :sneakers)
def test_retry_stopped_logging
perform_enqueued_jobs do
RetryJob.perform_later "CustomCatchError", 6
end
assert_match(/Stopped retrying RetryJob \(Job ID: .*?\) due to a CustomCatchError.*, which reoccurred on \d+ attempts\./, @logger.messages)
end
assert_match(/Stopped retrying RetryJob \(Job ID: .*?\) due to a CustomCatchError.*, which reoccurred on \d+ attempts\./, @logger.messages)
end
def test_retry_stopped_logging_without_block
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 6
rescue DefaultsError
assert_match(/Stopped retrying RetryJob \(Job ID: .*?\) due to a DefaultsError.*, which reoccurred on \d+ attempts\./, @logger.messages)
def test_retry_stopped_logging_without_block
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 6
rescue DefaultsError
assert_match(/Stopped retrying RetryJob \(Job ID: .*?\) due to a DefaultsError.*, which reoccurred on \d+ attempts\./, @logger.messages)
end
end
end

View File

@ -53,6 +53,19 @@ class QueueAdapterTest < ActiveJob::TestCase
assert_equal base_queue_adapter, child_job_three.queue_adapter, "child_job_three's queue adapter should remain unchanged"
end
test "should default to :async adapter if no adapters are set at all" do
ActiveJob::Base.disable_test_adapter
_queue_adapter_was = ActiveJob::Base._queue_adapter
_queue_adapter_name_was = ActiveJob::Base._queue_adapter_name
ActiveJob::Base._queue_adapter = ActiveJob::Base._queue_adapter_name = nil
assert_equal "async", ActiveJob::Base.queue_adapter_name
assert_kind_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter
ensure
ActiveJob::Base._queue_adapter = _queue_adapter_was
ActiveJob::Base._queue_adapter_name = _queue_adapter_name_was
end
test "should extract a reasonable name from a class instance" do
child_job = Class.new(ActiveJob::Base)
child_job.queue_adapter = ActiveJob::QueueAdapters::StubOneAdapter.new

View File

@ -20,10 +20,37 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase
end
def test_set_test_adapter
assert_kind_of ActiveJob::QueueAdapters::TestAdapter, queue_adapter
# The queue adapter the job uses depends on the Active Job config.
# See https://github.com/rails/rails/pull/48585 for logic.
expected = case ActiveJob::Base.queue_adapter_name.to_sym
when :test
ActiveJob::QueueAdapters::TestAdapter
when :inline
ActiveJob::QueueAdapters::InlineAdapter
when :async
ActiveJob::QueueAdapters::AsyncAdapter
when :backburner
ActiveJob::QueueAdapters::BackburnerAdapter
when :delayed_job
ActiveJob::QueueAdapters::DelayedJobAdapter
when :queue_classic
ActiveJob::QueueAdapters::QueueClassicAdapter
when :resque
ActiveJob::QueueAdapters::ResqueAdapter
when :sidekiq
ActiveJob::QueueAdapters::SidekiqAdapter
when :sneakers
ActiveJob::QueueAdapters::SneakersAdapter
when :sucker_punch
ActiveJob::QueueAdapters::SuckerPunchAdapter
else
raise NotImplementedError.new
end
assert_kind_of expected, queue_adapter
end
def test_does_not_perform_enqueued_jobs_by_default
assert_nil queue_adapter.perform_enqueued_jobs
assert_nil ActiveJob::QueueAdapters::TestAdapter.new.perform_enqueued_jobs
end
end

File diff suppressed because it is too large Load Diff

View File

@ -81,6 +81,17 @@ Upgrading from Rails 7.1 to Rails 7.2
For more information on changes made to Rails 7.2 please see the [release notes](7_2_release_notes.html).
### All tests now respect the `active_job.queue_adapter` config
If you have set `config.active_job.queue_adapter` in your `config/application.rb` or `config/environments/test.rb` file,
the adapter you selected was previously not used consistently across all tests. In some tests your adapter would be
used, but other tests would use the `TestAdapter`.
In Rails 7.2, all tests will respect the `queue_adapter` config if provided. This may cause test errors, if you had
set the `queue_adapter` config to something other than `:test`, but written tests in a way that was dependent on the `TestAdapter`.
If no config is provided, the `TestAdapter` will continue to be used.
Upgrading from Rails 7.0 to Rails 7.1
-------------------------------------

View File

@ -0,0 +1,217 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
module ApplicationTests
class ActiveJobAdapterTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
@old = ENV["PARALLEL_WORKERS"]
ENV["PARALLEL_WORKERS"] = "0"
build_app
end
def teardown
ENV["PARALLEL_WORKERS"] = @old
teardown_app
end
test "config set via application.rb" do
add_to_config "config.active_job.queue_adapter = :inline"
make_inline_test_file
assert_successful_test_run "integration/config_test.rb"
end
test "config set via environment config" do
add_to_config "config.active_job.queue_adapter = :async"
app_file "config/environments/test.rb", <<-RUBY
Rails.application.configure do
config.active_job.queue_adapter = :inline
end
RUBY
make_inline_test_file
assert_successful_test_run "integration/config_test.rb"
end
test "config is set for production, but test uses defaults" do
app_file "config/environments/production.rb", <<-RUBY
Rails.application.configure do
config.active_job.queue_adapter = :sidekiq
end
RUBY
make_test_test_file
assert_successful_test_run "integration/config_test.rb"
end
private
def make_inline_test_file
app_file "test/integration/config_test.rb", <<-RUBY
require "test_helper"
class RailsConfigUnitTest < ActiveSupport::TestCase
test "the Inline Active Job adapter is used in unit tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigIntegrationTest < ActionDispatch::IntegrationTest
test "the Inline Active Job adapter is used in integration tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
# ActionDispatch::IntegrationTest includes ActiveJob::TestHelper,
# which adds a bunch of assertions. But these assertions only work
# if the test adapter is TestAdapter. So for other test adapters,
# we raise an error if the method is called.
assert_raise ArgumentError, "assert_enqueued_jobs requires the Active Job test adapter, you're using ActiveJob::QueueAdapters::InlineAdapter" do
assert_no_enqueued_jobs {}
end
end
end
class RailsConfigJobTest < ActiveJob::TestCase
test "the Inline Active Job adapter is used in job tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
# ActiveJob::TesTCase includes ActiveJob::TestHelper,
# which adds a bunch of assertions. But these assertions only work
# if the test adapter is TestAdapter. So for other test adapters,
# we raise an error if the method is called.
assert_raise ArgumentError, "assert_enqueued_jobs requires the Active Job test adapter, you're using ActiveJob::QueueAdapters::InlineAdapter" do
assert_no_enqueued_jobs {}
end
end
end
class RailsConfigMailerTest < ActionMailer::TestCase
test "the Inline Active Job adapter is used in mailer tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
# ActionMailer::TestHelper includes ActiveJob::TestHelper
# So this just asserts that we haven't broken Action Mailer assertions that
# depend on Active Job:
assert_emails(0) {}
assert_emails(0)
end
end
class RailsConfigHelperTest < ActionView::TestCase
test "the Inline Active Job adapter is used in helper tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigControllerTest < ActionController::TestCase
test "the Inline Active Job adapter is used in controller tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigSystemTest < ActionDispatch::SystemTestCase
test "the Inline Active Job adapter is used in system tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter), adapter
assert_equal :inline, Rails.application.config.active_job.queue_adapter
end
end
RUBY
end
def make_test_test_file
app_file "test/integration/config_test.rb", <<-RUBY
require "test_helper"
class RailsConfigUnitTest < ActiveSupport::TestCase
test "the Test Active Job adapter is used in unit tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigIntegrationTest < ActionDispatch::IntegrationTest
test "the Test Active Job adapter is used in integration tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
assert_nothing_raised do
assert_no_enqueued_jobs {}
end
end
end
class RailsConfigJobTest < ActiveJob::TestCase
test "the Test Active Job adapter is used in job tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
assert_nothing_raised do
assert_no_enqueued_jobs {}
end
end
end
class RailsConfigMailerTest < ActionMailer::TestCase
test "the Test Active Job adapter is used in mailer tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
assert_emails(0) {}
assert_emails(0)
end
end
class RailsConfigHelperTest < ActionView::TestCase
test "the Test Active Job adapter is used in helper tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigControllerTest < ActionController::TestCase
test "the Test Active Job adapter is used in controller tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
end
end
class RailsConfigSystemTest < ActionDispatch::SystemTestCase
test "the Test Active Job adapter is used in system tests" do
adapter = ActiveJob::Base.queue_adapter
assert adapter.is_a?(ActiveJob::QueueAdapters::TestAdapter), adapter
assert_equal :test, Rails.application.config.active_job.queue_adapter
end
end
RUBY
end
def assert_successful_test_run(name)
result = run_test_file(name)
assert_equal 0, $?.to_i, result
result
end
def run_test_file(name)
rails "test", "#{app_path}/test/#{name}", allow_failure: true
end
end
end

View File

@ -4635,6 +4635,42 @@ module ApplicationTests
assert_equal "SQLite", ActiveRecord::Base.lease_connection.adapter_name
end
["development", "production"].each do |env|
test "active job adapter is async in #{env}" do
app(env)
assert_equal :async, Rails.application.config.active_job.queue_adapter
adapter = ActiveJob::Base.queue_adapter
assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, adapter
end
test "active job adapter can be overridden in #{env} via application.rb" do
add_to_config "config.active_job.queue_adapter = :inline"
app(env)
assert_equal :inline, Rails.application.config.active_job.queue_adapter
adapter = ActiveJob::Base.queue_adapter
assert_instance_of ActiveJob::QueueAdapters::InlineAdapter, adapter
end
test "active job adapter can be overridden in #{env} via environment config" do
app_file "config/environments/#{env}.rb", <<-RUBY
Rails.application.configure do
config.active_job.queue_adapter = :inline
end
RUBY
app(env)
assert_equal :inline, Rails.application.config.active_job.queue_adapter
adapter = ActiveJob::Base.queue_adapter
assert_instance_of ActiveJob::QueueAdapters::InlineAdapter, adapter
end
end
test "active job adapter is `:test` in test environment" do
app "test"
assert_equal :test, Rails.application.config.active_job.queue_adapter
adapter = ActiveJob::Base.queue_adapter
assert_instance_of ActiveJob::QueueAdapters::TestAdapter, adapter
end
private
def set_custom_config(contents, config_source = "custom".inspect)
app_file "config/custom.yml", contents