0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/spec/rails_helper.rb
Alan Guo Xiang Tan 6df9b0b357
DEV: Prefer RSpec primitives for test setup (#42122)
RSpec setup becomes harder to follow at either extreme: trivial fixture
wrappers hide lifecycle and intent, while forcing every named operation
inline repeats low-level protocol and configuration details.

This change documents and applies a test-setup hierarchy:

- use `fab!`, `let`, `let!`, `subject`, and inline `Fabricate` according
to lifecycle and role;
- use a small example-group method when parameterized behavior gives one
spec useful vocabulary;
- move helpers into auto-loaded `spec/support` only when they are shared
across spec files;
- use fabricators and page objects for the data shapes and system-test
interfaces they own.

Core and plugin support files are loaded centrally by `rails_helper`, so
plugin-specific support loaders are unnecessary. The migration specs
encountered during the sweep are removed according to repository policy;
production migrations are unchanged.
2026-07-30 11:17:22 +08:00

275 lines
9.7 KiB
Ruby
Vendored

# frozen_string_literal: true
if ENV["COVERAGE"]
require "simplecov"
if ENV["TEST_ENV_NUMBER"]
SimpleCov.command_name "#{SimpleCov.command_name} #{ENV["TEST_ENV_NUMBER"]}"
end
SimpleCov.start "rails" do
add_group "Libraries", %r{^/lib/(?!tasks).*$}
add_group "Scripts", "script"
add_group "Serializers", "app/serializers"
add_group "Services", "app/services"
add_group "Tasks", "lib/tasks"
end
end
require "rubygems"
require "rbtrace" if RUBY_ENGINE == "ruby"
require "pry"
require "pry-rails"
require "fabrication"
require "mocha/api"
require "certified"
require "webmock/rspec"
require_relative "support/server_error_tracking"
ENV["RAILS_ENV"] ||= "test"
ENV["ENABLE_LOGSTASH_LOGGER"] ||= "1"
require File.expand_path("../../config/environment", __FILE__)
Discourse.singleton_class.prepend(RspecWarnExceptionCapture)
require "rspec/rails"
require "shoulda-matchers"
require "sidekiq/testing"
require "capybara/rails"
# The shoulda-matchers gem no longer detects the test framework
# you're using or mixes itself into that framework automatically.
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :active_record
with.library :active_model
end
end
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/requests/examples/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/helpers/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/page_objects/**/base.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/page_objects/**/*_base.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/page_objects/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/fabricators/*.rb")].each { |f| require f }
require_relative "helpers/redis_snapshot_helper"
# Require plugin helpers at plugin/[plugin]/spec/plugin_helper.rb (includes symlinked plugins).
if ENV["LOAD_PLUGINS"] == "1"
Dir[Rails.root.join("plugins/*/spec/plugin_helper.rb")].each { |f| require f }
Dir[Rails.root.join("plugins/*/spec/support/**/*.rb")].sort.each { |f| require f }
Dir[Rails.root.join("plugins/*/spec/fabricators/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("plugins/*/spec/system/page_objects/**/*.rb")].each { |f| require f }
end
RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
# Default is :fork, but this causes problems if any miniracer context have started
config.bisect_runner = :shell
config.fail_fast = ENV["RSPEC_FAIL_FAST"] == "1"
config.silence_filter_announcements = ENV["RSPEC_SILENCE_FILTER_ANNOUNCEMENTS"] == "1"
config.extend RedisSnapshotHelper
config.include Helpers
config.include MessageBus
config.include RSpecHtmlMatchers
config.include IntegrationHelpers, type: :request
config.include SystemHelpers, type: :system
config.include ThemeScreenshotMarker, type: :system
config.include DiscourseWebauthnIntegrationHelpers
config.include SiteSettingsHelpers
config.include SidekiqHelpers
config.include UploadsHelpers
config.include BackupsHelpers
config.include OneboxHelpers
config.include FastImageHelpers
config.include ServiceMatchers
config.include I18nHelpers
config.include TimeHelpers
config.include AuthHelpers
config.include LoggingHelpers
config.order = "random"
config.infer_spec_type_from_file_location!
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
# Sometimes you may have a large string or object that you are comparing
# with some expectation, and you want to see the full diff between actual
# and expected without rspec truncating 90% of the diff. Setting the
# max_formatted_output_length to nil disables this truncation completely.
#
# c.f. https://www.rubydoc.info/gems/rspec-expectations/RSpec/Expectations/Configuration#max_formatted_output_length=-instance_method
if ENV["RSPEC_DISABLE_DIFF_TRUNCATION"]
config.expect_with :rspec do |expectation|
expectation.max_formatted_output_length = nil
end
end
# If true, the base class of anonymous controllers will be inferred
# automatically. This will be the default behavior in future versions of
# rspec-rails.
config.infer_base_class_for_anonymous_controllers = true
# Shows more than one line of backtrace in case of an error or spec failure.
config.full_cause_backtrace = false
# Sometimes the backtrace is quite big for failing specs, this will
# remove rspec/gem paths from the backtrace so it's easier to see the
# actual application code that caused the failure.
#
# This behaviour is enabled by default, to include gems in
# the backtrace set DISCOURSE_INCLUDE_GEMS_IN_RSPEC_BACKTRACE=1
if ENV["DISCOURSE_INCLUDE_GEMS_IN_RSPEC_BACKTRACE"] != "1"
config.backtrace_exclusion_patterns = [
%r{/lib\d*/ruby/},
%r{bin/},
/gems/,
%r{spec/spec_helper\.rb},
%r{spec/rails_helper\.rb},
%r{lib/rspec/(core|expectations|matchers|mocks)},
]
end
config.before(:suite) do
# Rebase the seeded DB settings as defaults, then swap in the in-memory provider.
TestLocalProcessProvider.install!
WebMock.disable_net_connect!(
allow_localhost: true,
allow: [
*MinioRunner.config.minio_urls,
URI(MinioRunner::MinioBinary.platform_binary_url).host,
ENV["CAPYBARA_REMOTE_DRIVER_URL"],
].compact,
)
# Registering this from inside before(:suite) makes it run at the end of the
# before(:each) chain. It must run after the specs' `sign_in`, so the auth
# cookie is made using the correct (current) time.
config.before(:each, type: :system) do |example|
if example.metadata[:time]
freeze_time(example.metadata[:time])
BrowserTime.freeze(page, example.metadata[:time])
end
end
# Prevents 500 errors for site setting URLs pointing to test.localhost in system specs.
SiteIconManager.clear_cache!
end
config.after(:suite) { Downloads.clear }
config.before(:each) { TestSetup.test_setup }
# Match the request hostname to the value in `database.yml`
config.before(:each, type: %i[request multisite system]) { host! "test.localhost" }
config.before(:each, type: :system) do |example|
SystemDrivers.preload_model_schemas!
SystemDrivers.register!(example)
driven_by SystemDrivers.driver_for(example)
setup_system_test
BlockRequestsMiddleware.current_example_location = example.location
# Suppress the "Before you post, please select a category or tag" education
# popup — it intercepts pointer events and makes system specs flaky when
# they click things in the composer.
SiteSetting.educate_until_posts = 0
SystemArtifacts.record_video(example)
SystemArtifacts.start_trace(page, example)
page.driver.with_playwright_page do |pw_page|
$playwright_logger = PlaywrightLogger.new(pw_page)
if (tz = example.metadata[:timezone])
BrowserTime.override_timezone(pw_page, tz)
end
end
end
config.before(:each) do |example|
if example.metadata[:type] != :system
EmberAssets.stubs(:read_manifest!).returns(nil)
EmberAssets.stubs(:script_chunks).returns({})
end
end
config.before(:each, type: :system) { MessageBusTestSync.start }
config.before(:each, type: :system) do |example|
EmberDeprecations.set_raise_on_deprecation!(example)
end
config.after(:each) do |example|
if example.exception && RspecErrorTracker.exceptions.present?
lines = (example.metadata[:extra_failure_lines] ||= +"")
RspecErrorTracker.append_failure_dump(lines)
end
unfreeze_time
ActionMailer::Base.deliveries.clear
Discourse.redis.flushdb
Scheduler::Defer.do_all_work
clear_mocked_upcoming_change_metadata
clear_mocked_upcoming_change_default_overrides
end
config.after(:each, type: :system) do |example|
SystemArtifacts.stop_trace(page, example)
lines = example.metadata[:extra_failure_lines]
if example.exception &&
(backtraces = example.metadata[:_capybara_server_threads_backtraces]).present?
CapybaraTimeoutExtension.append_server_thread_backtraces(lines, backtraces)
end
# Recommended that this is not disabled, since it makes debugging
# failed system tests a lot trickier.
if ENV["PLAYWRIGHT_DISABLE_VERBOSE_JS_LOGS"].blank? && $playwright_logger && example.exception
$playwright_logger.append_failure_logs(lines)
end
deprecation_error = EmberDeprecations.fatal_error($playwright_logger&.logs)
expect(deprecation_error).to be_nil, deprecation_error
EmberDeprecations.record_counts($playwright_logger&.logs, example.metadata)
page.execute_script("if (typeof MessageBus !== 'undefined') { MessageBus.stop(); }")
# Block all incoming requests before resetting Capybara session which will wait for all requests to finish
BlockRequestsMiddleware.block_requests!
Capybara.reset_session!
MessageBus.backend_instance.reset! # Clears all existing backlog from memory backend
end
# Registered last so that, running in reverse order, it executes before the
# teardown hook above. `MessageBusTestSync.stop` must come before the timeout
# re-raise, otherwise the raise would skip it.
config.after(:each, type: :system) do |example|
MessageBusTestSync.stop
# If test passed, but we had a capybara finder timeout, raise it now
if example.exception.nil? &&
(capybara_timeout_error = example.metadata[:_capybara_timeout_exception])
raise capybara_timeout_error
end
end
end