lib/rubocop/cop/rspec/subject_stub.rb in rubocop-rspec-2.7.0 vs lib/rubocop/cop/rspec/subject_stub.rb in rubocop-rspec-2.8.0
- old
+ new
@@ -5,10 +5,13 @@
module RuboCop
module Cop
module RSpec
# Checks for stubbed test subjects.
#
+ # Checks nested subject stubs for innermost subject definition
+ # when subject is also defined in parent example groups.
+ #
# @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
# @see https://samphippen.com/introducing-rspec-smells-and-where-to-find-them#smell-1-stubject
# @see https://github.com/rubocop-hq/rspec-style-guide#dont-stub-subject
#
# @example
@@ -20,10 +23,24 @@
# allow(article).to receive(:author).and_return(nil)
# expect(article.description).to include('by an unknown author')
# end
# end
#
+ # # bad
+ # describe Article do
+ # subject(:foo) { Article.new }
+ #
+ # context 'nested subject' do
+ # subject(:article) { Article.new }
+ #
+ # it 'indicates that the author is unknown' do
+ # allow(article).to receive(:author).and_return(nil)
+ # expect(article.description).to include('by an unknown author')
+ # end
+ # end
+ # end
+ #
# # good
# describe Article do
# subject(:article) { Article.new(author: nil) }
#
# it 'indicates that the author is unknown' do
@@ -34,33 +51,41 @@
class SubjectStub < Base
include TopLevelGroup
MSG = 'Do not stub methods of the object under test.'
- # @!method subject(node)
+ # @!method subject?(node)
# Find a named or unnamed subject definition
#
# @example anonymous subject
- # subject(parse('subject { foo }').ast) do |name|
+ # subject?(parse('subject { foo }').ast) do |name|
# name # => :subject
# end
#
# @example named subject
- # subject(parse('subject(:thing) { foo }').ast) do |name|
+ # subject?(parse('subject(:thing) { foo }').ast) do |name|
# name # => :thing
# end
#
# @param node [RuboCop::AST::Node]
#
# @yield [Symbol] subject name
- def_node_matcher :subject, <<-PATTERN
- (block
- (send nil?
- {:subject (sym $_) | $:subject}
- ) args ...)
+ def_node_matcher :subject?, <<-PATTERN
+ (block
+ (send nil?
+ {:subject (sym $_) | $:subject}
+ ) args ...)
PATTERN
+ # @!method let?(node)
+ # Find a memoized helper
+ def_node_matcher :let?, <<-PATTERN
+ (block
+ (send nil? :let (sym $_)
+ ) args ...)
+ PATTERN
+
# @!method message_expectation?(node, method_name)
# Match `allow` and `expect(...).to receive`
#
# @example source that matches
# allow(foo).to receive(:bar)
@@ -71,11 +96,11 @@
# expect(foo).to receive(:bar).with(1).and_return(2)
#
def_node_matcher :message_expectation?, <<-PATTERN
(send
{
- (send nil? { :expect :allow } (send nil? {% :subject}))
+ (send nil? { :expect :allow } (send nil? %))
(send nil? :is_expected)
}
#Runners.all
#message_expectation_matcher?
)
@@ -87,41 +112,42 @@
:receive :receive_messages :receive_message_chain :have_received
} ...)
PATTERN
def on_top_level_group(node)
- @explicit_subjects = find_all_explicit_subjects(node)
+ @explicit_subjects = find_all_explicit(node, &method(:subject?))
+ @subject_overrides = find_all_explicit(node, &method(:let?))
find_subject_expectations(node) do |stub|
add_offense(stub)
end
end
private
- def find_all_explicit_subjects(node)
+ def find_all_explicit(node)
node.each_descendant(:block).with_object({}) do |child, h|
- name = subject(child)
+ name = yield(child)
next unless name
- outer_example_group = child.each_ancestor.find do |a|
+ outer_example_group = child.each_ancestor(:block).find do |a|
example_group?(a)
end
h[outer_example_group] ||= []
h[outer_example_group] << name
end
end
def find_subject_expectations(node, subject_names = [], &block)
- subject_names = @explicit_subjects[node] if @explicit_subjects[node]
+ subject_names = [*subject_names, *@explicit_subjects[node]]
+ subject_names -= @subject_overrides[node] if @subject_overrides[node]
- expectation_detected = (subject_names + [:subject]).any? do |name|
- message_expectation?(node, name)
- end
+ names = Set[*subject_names, :subject]
+ expectation_detected = message_expectation?(node, names)
return yield(node) if expectation_detected
- node.each_child_node do |child|
+ node.each_child_node(:send, :def, :block, :begin) do |child|
find_subject_expectations(child, subject_names, &block)
end
end
end
end