diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac37f7..09894b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,8 @@ jobs: POSTGRES_HOST: localhost POSTGRES_PORT: 5432 - name: Run specs - run: bundle exec rake install_database_yml spec + run: + bundle exec rake install_database_yml spec && bundle exec rake testing_spec env: BACKTRACE: 1 BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/CHANGELOG.md b/CHANGELOG.md index de0ceeb..d2010d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added means to disable tracking ignored tables +- Allow enabling/disabling IronTrail in rspec ## 0.0.1 - 2024-11-26 diff --git a/README.md b/README.md index e8ef337..a3371e1 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,32 @@ RSpec.configure do |config| end ``` +You'll likely also want to require [lib/iron_trail/testing/rspec.rb](lib/iron_trail/testing/rspec.rb) +in your `rails_helper.rb`, then explicitly either disable or enable IronTrail in tests: + +```ruby +require 'iron_trail/testing/rspec' +IronTrail::Testing.enable! # to have it enabled by default in specs +IronTrail::Testing.disable! # to have it disabled by default in specs +``` + +You don't make it explicit, IronTrail will be enabled by default, which will +likely impact your test suite performance slightly. + +In case you disable it by default, you can enable it per rspec context with: + +```ruby +describe 'in a "describe" block', iron_trail: true do + it 'or also in an "it" block', iron_trail: true do + # ... + end +end +``` + +Enabling/disabling IronTrail in specs works by replacing the trigger function in Postgres +with a dummy no-op function or with the real function and it won't add or drop triggers from +any tables. + ## Rake tasks IronTrail comes with a few handy rake tasks you can use in your dev, test and diff --git a/Rakefile b/Rakefile index 301e306..82b9eab 100644 --- a/Rakefile +++ b/Rakefile @@ -75,4 +75,12 @@ require 'rspec/core/rake_task' task(:spec).clear RSpec::Core::RakeTask.new(:spec) -task default: %i[prepare spec] +# Loading the testing/rspec file will affect RSpec globally. Because of that, +# we want to test it in a separate scope. We could also always require it, +# but that wouldn't be true in a real rails app and could make all tests +# farther apart from reality, thus less reliable. +RSpec::Core::RakeTask.new(:testing_spec).tap do |task| + task.pattern = 'spec/testing_itself.rb' +end + +task default: %i[prepare spec testing_spec] diff --git a/lib/iron_trail/testing/rspec.rb b/lib/iron_trail/testing/rspec.rb index 1a7503b..d9fc5f3 100644 --- a/lib/iron_trail/testing/rspec.rb +++ b/lib/iron_trail/testing/rspec.rb @@ -1,3 +1,66 @@ # frozen_string_literal: true +if ENV['RAILS_ENV'] == 'production' + raise 'This file should not be required in production. ' \ + 'Change the RAILS_ENV env var temporarily to override this.' +end + require 'iron_trail' + +module IronTrail + module Testing + class << self + attr_accessor :enabled + + def enable! + DbFunctions.new(ActiveRecord::Base.connection).install_functions + @enabled = true + end + + def disable! + # We "disable" it by replacing the trigger function by a no-op one. + # This should be faster than adding/removing triggers from several + # tables every time. + sql = <<~SQL + CREATE OR REPLACE FUNCTION irontrail_log_row() + RETURNS TRIGGER AS $$ + BEGIN + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + SQL + + ActiveRecord::Base.connection.execute(sql) + @enabled = false + end + + def with_iron_trail(want_enabled:, &block) + was_enabled = IronTrail::Testing.enabled + + if want_enabled + ::IronTrail::Testing.enable! unless was_enabled + else + ::IronTrail::Testing.disable! if was_enabled + end + + block.call + ensure + if want_enabled && !was_enabled + ::IronTrail::Testing.disable! + elsif !want_enabled && was_enabled + ::IronTrail::Testing.enable! + end + end + end + end +end + +RSpec.configure do |config| + config.around(:each, iron_trail: true) do |example| + IronTrail::Testing.with_iron_trail(want_enabled: true) { example.run } + end + config.around(:each, iron_trail: false) do |example| + raise "Using iron_trail: false does not do what you might think it does. To disable iron_trail, " \ + "use IronTrail::Testing.with_iron_trail(want_enabled: false) { ... } instead." + end +end diff --git a/spec/testing_itself.rb b/spec/testing_itself.rb new file mode 100644 index 0000000..ea31d4d --- /dev/null +++ b/spec/testing_itself.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'iron_trail/testing/rspec' + +IronTrail::Testing.disable! + +RSpec.describe 'lib/iron_trail/testing/rspec.rb' do + let(:person) { Person.create!(first_name: 'Arthur', last_name: 'Schopenhauer') } + + subject(:do_some_changes!) do + person.update!(first_name: 'Jim') + person.update!(first_name: 'Jane') + end + + describe 'IronTrail::Testing#with_iron_trail' do + context 'when IronTrail is disabled but we enable it for a while' do + it 'tracks only while enabled' do + person.update!(first_name: 'Jim') + + expect(person.reload.iron_trails.length).to be(0) + + IronTrail::Testing.with_iron_trail(want_enabled: true) do + person.update!(first_name: 'Jane') + end + + expect(person.reload.iron_trails.length).to be(1) + + person.update!(first_name: 'Joe') + + expect(person.reload.iron_trails.length).to be(1) + end + end + end + + describe 'rspec helpers' do + context 'with IronTrail disabled' do + it 'does not track anything' do + do_some_changes! + + expect(person.reload.iron_trails.length).to be(0) + end + end + + context 'with IronTrail enabled through the helper', iron_trail: true do + it 'does not track anything' do + do_some_changes! + + expect(person.reload.iron_trails.length).to be(3) + end + end + end +end