From 8aad7257f2055f3fce98c17bd63465bac6801517 Mon Sep 17 00:00:00 2001 From: Dmitriy Ivliev <3938172+moofkit@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:35:31 +0300 Subject: [PATCH 1/2] avoid include active record test fixtures if use_active_record = false` --- .../spec/verify_no_fixture_setup_spec.rb | 17 +---- lib/rspec/rails/fixture_support.rb | 72 +++++++++---------- spec/rspec/rails/fixture_support_spec.rb | 20 ++++++ 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/example_app_generator/no_active_record/spec/verify_no_fixture_setup_spec.rb b/example_app_generator/no_active_record/spec/verify_no_fixture_setup_spec.rb index d3ec9b7e6b..ef1a945628 100644 --- a/example_app_generator/no_active_record/spec/verify_no_fixture_setup_spec.rb +++ b/example_app_generator/no_active_record/spec/verify_no_fixture_setup_spec.rb @@ -1,21 +1,6 @@ -# Pretend that ActiveRecord::Rails is defined and this doesn't blow up -# with `config.use_active_record = false`. -# Trick the other spec that checks that ActiveRecord is -# *not* defined by wrapping it in RSpec::Rails namespace -# so that it's reachable from RSpec::Rails::FixtureSupport. -# NOTE: this has to be defined before requiring `rails_helper`. -module RSpec - module Rails - module ActiveRecord - module TestFixtures - end - end - end -end - require 'rails_helper' -RSpec.describe 'Example App', :use_fixtures, type: :model do +RSpec.describe 'Example App', type: :model do it "does not set up fixtures" do expect(defined?(fixtures)).not_to be end diff --git a/lib/rspec/rails/fixture_support.rb b/lib/rspec/rails/fixture_support.rb index 564a829b7c..85ff9d432c 100644 --- a/lib/rspec/rails/fixture_support.rb +++ b/lib/rspec/rails/fixture_support.rb @@ -2,52 +2,50 @@ module RSpec module Rails # @private module FixtureSupport - if defined?(ActiveRecord::TestFixtures) - extend ActiveSupport::Concern - include RSpec::Rails::SetupAndTeardownAdapter - include RSpec::Rails::MinitestLifecycleAdapter - include RSpec::Rails::MinitestAssertionAdapter - include ActiveRecord::TestFixtures - - # @private prevent ActiveSupport::TestFixtures to start a DB transaction. - # Monkey patched to avoid collisions with 'let(:name)' since Rails 6.1 - def run_in_transaction? - current_example_name = (RSpec.current_example && RSpec.current_example.metadata[:description]) - use_transactional_tests && !self.class.uses_transaction?(current_example_name) - end - - included do - if RSpec.configuration.use_active_record? - include Fixtures + extend ActiveSupport::Concern + + included do + if RSpec.configuration.use_active_record? && defined?(ActiveRecord::TestFixtures) + include RSpec::Rails::SetupAndTeardownAdapter + include RSpec::Rails::MinitestLifecycleAdapter + include RSpec::Rails::MinitestAssertionAdapter + include ActiveRecord::TestFixtures + include Fixtures + + # @private prevent ActiveSupport::TestFixtures to start a DB transaction. + # Monkey patched to avoid collisions with 'let(:name)' since Rails 6.1 + def run_in_transaction? + current_example_name = (RSpec.current_example && RSpec.current_example.metadata[:description]) + use_transactional_tests && !self.class.uses_transaction?(current_example_name) + end - self.fixture_paths = RSpec.configuration.fixture_paths + self.fixture_paths = RSpec.configuration.fixture_paths - self.use_transactional_tests = RSpec.configuration.use_transactional_fixtures - self.use_instantiated_fixtures = RSpec.configuration.use_instantiated_fixtures + self.use_transactional_tests = RSpec.configuration.use_transactional_fixtures + self.use_instantiated_fixtures = RSpec.configuration.use_instantiated_fixtures - fixtures RSpec.configuration.global_fixtures if RSpec.configuration.global_fixtures - end + fixtures RSpec.configuration.global_fixtures if RSpec.configuration.global_fixtures end + end - module Fixtures - extend ActiveSupport::Concern + module Fixtures + extend ActiveSupport::Concern - class_methods do - def fixtures(*args) - super.tap do - fixture_sets.each_pair do |method_name, fixture_name| - proxy_method_warning_if_called_in_before_context_scope(method_name, fixture_name) - end + class_methods do + def fixtures(*args) + super.tap do + fixture_sets.each_pair do |method_name, fixture_name| + proxy_method_warning_if_called_in_before_context_scope(method_name, fixture_name) end end + end - def proxy_method_warning_if_called_in_before_context_scope(method_name, fixture_name) - define_method(method_name) do |*args, **kwargs, &blk| - if RSpec.current_scope == :before_context_hook - RSpec.warn_with("Calling fixture method in before :context ") - else - access_fixture(fixture_name, *args, **kwargs, &blk) - end + def proxy_method_warning_if_called_in_before_context_scope(method_name, fixture_name) + define_method(method_name) do |*args, **kwargs, &blk| + if RSpec.current_scope == :before_context_hook + RSpec.warn_with("Calling fixture method in before :context ") + else + access_fixture(fixture_name, *args, **kwargs, &blk) end end end diff --git a/spec/rspec/rails/fixture_support_spec.rb b/spec/rspec/rails/fixture_support_spec.rb index 1094486f0b..2633f726d7 100644 --- a/spec/rspec/rails/fixture_support_spec.rb +++ b/spec/rspec/rails/fixture_support_spec.rb @@ -69,5 +69,25 @@ def expect_to_pass(group) failure_reporter.exceptions.map { |e| raise e } expect(result).to be true end + + context "with use_active_record set to false" do + it "does not support fixture_path/fixture_paths" do + allow(RSpec.configuration).to receive(:use_active_record) { false } + group = RSpec::Core::ExampleGroup.describe do + include FixtureSupport + end + + expect(group).not_to respond_to(:fixture_paths) + end + + it "does not include ActiveRecord::TestFixtures" do + allow(RSpec.configuration).to receive(:use_active_record) { false } + group = RSpec::Core::ExampleGroup.describe do + include FixtureSupport + end + + expect(group).not_to include(ActiveRecord::TestFixtures) + end + end end end From 5dbde4997ace0a6b64209767727a6a5a3b1db4d6 Mon Sep 17 00:00:00 2001 From: Dmitriy Ivliev <3938172+moofkit@users.noreply.github.com> Date: Sat, 25 Oct 2025 13:27:14 +0300 Subject: [PATCH 2/2] improved transactions documentation --- features/.nav | 2 +- features/ActiveRecord.md | 181 ++++++++++++++++++ features/Transactions.md | 99 ---------- .../install/templates/spec/rails_helper.rb | 7 +- 4 files changed, 185 insertions(+), 104 deletions(-) create mode 100644 features/ActiveRecord.md delete mode 100644 features/Transactions.md diff --git a/features/.nav b/features/.nav index 0af57bfb15..e419d3eeee 100644 --- a/features/.nav +++ b/features/.nav @@ -1,6 +1,6 @@ - GettingStarted.md (Start from scratch) - Generators.md (Generators) -- Transactions.md +- ActiveRecord.md - directory_structure.feature - backtrace_filtering.feature - model_specs: diff --git a/features/ActiveRecord.md b/features/ActiveRecord.md new file mode 100644 index 0000000000..4736f326ad --- /dev/null +++ b/features/ActiveRecord.md @@ -0,0 +1,181 @@ +# Active Record + +`rspec-rails` by default injects [ActiveSupport::TestCase](https://api.rubyonrails.org/classes/ActiveSupport/TestCase.html) and exposes some of the settings to RSpec configuration. +Furthermore it adds special hooks into `before` and `after` which are essential for [Active Record Fixtures](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html) and keeping the database in a clean state. + +It provides the `fixtures` class method in the rspec context to tell Rails which fixtures to prepare before each example. + +In addition to being available in the database, the fixture’s data may also be accessed by using a special dynamic method, which has the same name as the model. + +```ruby +RSpec.configure do |config| + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] +end + +RSpec.describe Thing, type: :model do + fixtures :things + + it "fixture method defined" do + expect(things(:one)).to eq(Thing.find_by(name: "one")) + end +end +``` + +More details on how to use fixtures are in the [Rails documentation](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Using+Fixtures+in+Test+Cases) + +## Transactions + +When you run `rails generate rspec:install`, the `spec/rails_helper.rb` file +includes the following configuration: + +```ruby +RSpec.configure do |config| + config.use_transactional_fixtures = true +end +``` + +The name of this setting is a bit misleading. What it really means in Rails +is "run every test method within a transaction." In the context of rspec-rails, +it means "run every example within a transaction." + +The idea is to start each example with a clean database, create whatever data +is necessary for that example, and then remove that data by simply rolling back +the transaction at the end of the example. + +For how this affects methods exposing transaction visibility see: +https://guides.rubyonrails.org/testing.html#transactions + +### Data created in `before` are rolled back + +Any data you create in a `before` hook will be rolled back at the end of +the example. This is a good thing because it means that each example is +isolated from state that would otherwise be left around by the examples that +already ran. For example: + +```ruby +describe Widget do + before do + @widget = Widget.create + end + + it "does something" do + expect(@widget).to do_something + end + + it "does something else" do + expect(@widget).to do_something_else + end +end +``` + +The `@widget` is recreated in each of the two examples above, so each example +has a different object, _and_ the underlying data is rolled back so the data +backing the `@widget` in each example is new. + +### Data created in `before(:context)` are _not_ rolled back + +`before(:context)` hooks are invoked before the transaction is opened. You can use +this to speed things up by creating data once before any example in a group is +run, however, this introduces a number of complications and you should only do +this if you have a firm grasp of the implications. Here are a couple of +guidelines: + +1. Be sure to clean up any data in an `after(:context)` hook: + + ```ruby + before(:context) do + @widget = Widget.create! + end + + after(:context) do + @widget.destroy + end + ``` + + If you don't do that, you'll leave data lying around that will eventually +interfere with other examples. + +2. Reload the object in a `before` hook. + + ```ruby + before(:context) do + @widget = Widget.create! + end + + before do + @widget.reload + end + ``` + +Even though database updates in each example will be rolled back, the +object won't _know_ about those rollbacks so the object and its backing +data can easily get out of sync. + +## Configuration + +### Disabling Active Record support + +If you prefer to manage the data yourself, or using another tool like +[database_cleaner](https://github.com/bmabey/database_cleaner) to do it for you, +simply tell RSpec to tell Rails not to manage fixtures and cleaning database. + +```ruby +RSpec.configure do |config| + config.use_active_record = false # is true by default +end +``` + +### Fixtures path + +The generator will provide the default path to the fixture, but it is possible to change it: +```ruby +RSpec.configure do |config| + config.fixture_paths = Rails.root.join('some/dir') # Rails.root.join('spec/fixtures') by default +end +``` + +### Instantiated fixtures + +If you want to have your fixtures available as an instance variable in the example, you could use `use_instantiated_fixtures` option: + +```ruby +RSpec.configure do |config| + config.use_instantiated_fixtures = true # false, by default +end + +RSpec.describe Thing, type: :model do + fixtures :things + + it "instantiates fixtures" do + expect(@things["one"]).to eq(@one) + end +end +``` + +### Global fixtures + +Sometimes it is required to have some fixture in each example, and it's possible to do this via `global_fixtures` setting: + +```ruby +RSpec.configure do |config| + config.global_fixtures = [:things] +end + +RSpec.describe Thing, type: :model do + it "inserts fixture" do + expect(things(:one)).to be_a(Thing) + end +end +``` + +### Disabling transactions + +If your database does not support transactions, but you still want to use Rails fixtures, it is possible to disable transactions explicitly: + +```ruby +RSpec.configure do |config| + config.use_transactional_fixtures = false +end +``` diff --git a/features/Transactions.md b/features/Transactions.md deleted file mode 100644 index c5237b07d8..0000000000 --- a/features/Transactions.md +++ /dev/null @@ -1,99 +0,0 @@ -# Transactions - -When you run `rails generate rspec:install`, the `spec/rails_helper.rb` file -includes the following configuration: - -```ruby -RSpec.configure do |config| - config.use_transactional_fixtures = true -end -``` - -The name of this setting is a bit misleading. What it really means in Rails -is "run every test method within a transaction." In the context of rspec-rails, -it means "run every example within a transaction." - -The idea is to start each example with a clean database, create whatever data -is necessary for that example, and then remove that data by simply rolling back -the transaction at the end of the example. - -For how this affects methods exposing transaction visibility see: -https://guides.rubyonrails.org/testing.html#transactions - -### Disabling transactions - -If you prefer to manage the data yourself, or using another tool like -[database_cleaner](https://github.com/bmabey/database_cleaner) to do it for you, -simply tell RSpec to tell Rails not to manage transactions: - -```ruby -RSpec.configure do |config| - config.use_transactional_fixtures = false -end -``` - -### Data created in `before(:example)` are rolled back - -Any data you create in a `before(:example)` hook will be rolled back at the end of -the example. This is a good thing because it means that each example is -isolated from state that would otherwise be left around by the examples that -already ran. For example: - -```ruby -describe Widget do - before(:example) do - @widget = Widget.create - end - - it "does something" do - expect(@widget).to do_something - end - - it "does something else" do - expect(@widget).to do_something_else - end -end -``` - -The `@widget` is recreated in each of the two examples above, so each example -has a different object, _and_ the underlying data is rolled back so the data -backing the `@widget` in each example is new. - -### Data created in `before(:context)` are _not_ rolled back - -`before(:context)` hooks are invoked before the transaction is opened. You can use -this to speed things up by creating data once before any example in a group is -run, however, this introduces a number of complications and you should only do -this if you have a firm grasp of the implications. Here are a couple of -guidelines: - -1. Be sure to clean up any data in an `after(:context)` hook: - - ```ruby - before(:context) do - @widget = Widget.create! - end - - after(:context) do - @widget.destroy - end - ``` - - If you don't do that, you'll leave data lying around that will eventually -interfere with other examples. - -2. Reload the object in a `before(:example)` hook. - - ```ruby - before(:context) do - @widget = Widget.create! - end - - before(:example) do - @widget.reload - end - ``` - -Even though database updates in each example will be rolled back, the -object won't _know_ about those rollbacks so the object and its backing -data can easily get out of sync. diff --git a/lib/generators/rspec/install/templates/spec/rails_helper.rb b/lib/generators/rspec/install/templates/spec/rails_helper.rb index 0840d3dc25..095bb38ea3 100644 --- a/lib/generators/rspec/install/templates/spec/rails_helper.rb +++ b/lib/generators/rspec/install/templates/spec/rails_helper.rb @@ -45,12 +45,11 @@ Rails.root.join('spec/fixtures') ] - # 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. + # If 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 - # You can uncomment this line to turn off ActiveRecord support entirely. + # You can uncomment this line to turn off ActiveRecord support entirely # config.use_active_record = false <% else -%>