Skip to content

Commit

Permalink
add report_duplicates config option for let_it_be
Browse files Browse the repository at this point in the history
  • Loading branch information
lHydra committed Jun 3, 2024
1 parent 29e6ac8 commit 4806b1f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master (unreleased)

- Add support for `report_duplicates` config option for `let_it_be` ([@lHydra][])

- Vernier: Add hooks configuration parameter. ([@lHydra][])

Now you can add more insights to the resulting report by adding event markers from Active Support Notifications.
Expand Down
33 changes: 33 additions & 0 deletions docs/recipes/let_it_be.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,36 @@ end
And then tag contexts/examples with `:let_it_be_frost` to enable this feature.

Alternatively, you can specify `freeze` modifier explicitly (`let_it_be(freeze: true)`) or configure an alias.

## Report duplicates

Although we suggest using `let_it_be` instead of `let!`, there is one important difference: you can override `let!` definition with the same or nested context, so only the latter one is called; `let_it_be` records could be overridden, but still created. For example:

```ruby
context "A" do
let!(:user) { create(:user, name: "a") }
let_it_be(:post) { create(:post, title: "A") }

specify { expect(User.all.pluck(:name)).to eq ["a"] }
specify { expect(Post.all.pluck(:title)).to eq ["A"] }

context "B" do
let!(:user) { create(:user, name: "b") }
let_it_be(:post) { create(:post, title: "B") }

specify { expect(User.all.pluck(:name)).to eq ["b"] }
specify { expect(Post.all.pluck(:title)).to eq ["B"] } # fails, because there are two posts
end
end
```

So for your convenience, you can configure the behavior when let_it_be is overridden.

```ruby
TestProf::LetItBe.configure do |config|
config.report_duplicates = :warn # Rspec.warn_with
config.report_duplicates = :raise # Kernel.raise
end
```

By default this parameter is disabled. You can configure the behavior that will generate a warning or raise an exception.
29 changes: 29 additions & 0 deletions lib/test_prof/recipes/rspec/let_it_be.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module TestProf
# Just like `let`, but persist the result for the whole group.
# NOTE: Experimental and magical, for more control use `before_all`.
module LetItBe
class DuplicationError < StandardError; end

Modifier = Struct.new(:scope, :block) do
def call(record, config)
block.call(record, config)
Expand All @@ -32,6 +34,19 @@ def register_modifier(key, on: :let, &block)
LetItBe.modifiers[key] = Modifier.new(on, block)
end

def report_duplicates=(value)
value = value.to_sym
unless %i[warn raise].include?(value)
raise ArgumentError, "#{value} is not acceptable, acceptable values are :warn or :raise"
end

@report_duplicates = value
end

def report_duplicates
@report_duplicates ||= false
end

def default_modifiers
@default_modifiers ||= {}
end
Expand Down Expand Up @@ -117,6 +132,8 @@ def let_it_be(identifier, **options, &block)
instance_variable_get(:"#{PREFIX}#{identifier}")
end

report_duplicates(identifier) if LetItBe.config.report_duplicates

LetItBe.module_for(self).module_eval do
define_method(identifier) do
# Trying to detect the context
Expand All @@ -135,6 +152,18 @@ def let_it_be(identifier, **options, &block)
let(identifier, &let_accessor)
end

private def report_duplicates(identifier)
if instance_methods.include?(identifier) && File.basename(__FILE__) == File.basename(instance_method(identifier).source_location[0])
error_msg = "let_it_be(:#{identifier}) was redefined in nested group"

if LetItBe.config.report_duplicates == :warn
::RSpec.warn_with(error_msg)
else
raise DuplicationError, error_msg
end
end
end

module Freezer
# Stoplist to prevent freezing objects and theirs associations that are defined
# with `let_it_be`'s `freeze: false` options during deep freezing.
Expand Down
180 changes: 180 additions & 0 deletions spec/integrations/fixtures/rspec/let_it_be_nested_fixture.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# frozen_string_literal: true

require_relative "../../../support/ar_models"
require_relative "../../../support/transactional_context"

require "test_prof/recipes/rspec/let_it_be"

RSpec.describe "Overriding detection", :transactional do
context "when report_duplicates was set as :raise" do
context "when let_it_be redefined" do
context "when on same nested level" do
it "raises a duplication error" do
expect do
TestProf::LetItBe.configure do |config|
config.report_duplicates = :raise
end

RSpec.describe "let_it_be on same nested level" do
include TestProf::FactoryBot::Syntax::Methods

let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user) }
end
end.to raise_error(TestProf::LetItBe::DuplicationError)
end
end

context "when nested level is 2" do
it "raises a duplication error" do
expect do
TestProf::LetItBe.configure do |config|
config.report_duplicates = :raise
end

RSpec.describe "let_it_be in nested context" do
include TestProf::FactoryBot::Syntax::Methods

let_it_be(:user) { create(:user) }

context "nested context level 2" do
let_it_be(:user) { create(:user) }
end
end
end.to raise_error(TestProf::LetItBe::DuplicationError)
end
end

context "when nested level is 3" do
it "raises a duplication error" do
expect do
TestProf::LetItBe.configure do |config|
config.report_duplicates = :raise
end

RSpec.describe "let_it_be in nested context" do
include TestProf::FactoryBot::Syntax::Methods

let_it_be(:user) { create(:user) }

context "nested context level 2" do
context "nested context level 3" do
let_it_be(:user) { create(:user) }
end
end
end
end.to raise_error(TestProf::LetItBe::DuplicationError)
end
end
end

context "when defined let and let_it_be" do
it "does not raise a duplication error" do
expect do
TestProf::LetItBe.configure do |config|
config.report_duplicates = :raise
end

RSpec.describe "let_it_be and let" do
include TestProf::FactoryBot::Syntax::Methods

let(:user) { create(:user) }

context "nested context level 2" do
let_it_be(:user) { create(:user) }
end
end
end.not_to raise_error
end
end
end

context "when report_duplicates was set as :warn" do
let(:warning_msg) { "let_it_be(:user) was redefined in nested group" }

before do
allow(::RSpec).to receive(:warn_with).with(warning_msg)
end

context "when let_it_be redefined" do
context "when on same nested level" do
it "warns a duplication message" do
RSpec.describe "let_it_be on same nested level" do
include TestProf::FactoryBot::Syntax::Methods

TestProf::LetItBe.configure do |config|
config.report_duplicates = :warn
end

let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user) }
end.run

expect(::RSpec).to have_received(:warn_with).with(warning_msg).once
end
end

context "when nested level is 2" do
it "warns a duplication message" do
RSpec.describe "let_it_be in nested context" do
include TestProf::FactoryBot::Syntax::Methods

TestProf::LetItBe.configure do |config|
config.report_duplicates = :warn
end

let_it_be(:user) { create(:user) }

context "nested context" do
let_it_be(:user) { create(:user) }
end
end.run

expect(::RSpec).to have_received(:warn_with).with(warning_msg).once
end
end

context "when nested level is 3" do
it "warns a duplication message" do
RSpec.describe "let_it_be in nested context" do
include TestProf::FactoryBot::Syntax::Methods

TestProf::LetItBe.configure do |config|
config.report_duplicates = :warn
end

let_it_be(:user) { create(:user) }

context "nested context level 2" do
context "nested context level 3" do
let_it_be(:user) { create(:user) }
end
end
end.run

expect(::RSpec).to have_received(:warn_with).with(warning_msg).once
end
end
end

context "when defined let and let_it_be" do
it "does not warn a duplication message" do
RSpec.describe "let_it_be and let" do
include TestProf::FactoryBot::Syntax::Methods

TestProf::LetItBe.configure do |config|
config.report_duplicates = :raise
end

let(:user) { create(:user) }

context "nested context level 2" do
let_it_be(:user) { create(:user) }
end
end.run

expect(::RSpec).not_to have_received(:warn_with)
end
end
end
end
6 changes: 6 additions & 0 deletions spec/integrations/let_it_be_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@

expect(output).to include("0 failures")
end

specify "it detects let_it_be override" do
output = run_rspec("let_it_be_nested")

expect(output).to include("0 failures")
end
end

0 comments on commit 4806b1f

Please sign in to comment.