Skip to content

Commit

Permalink
Add Stubject Cop
Browse files Browse the repository at this point in the history
  • Loading branch information
backus committed Aug 6, 2016
1 parent a0c451c commit d050adb
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 0 deletions.
4 changes: 4 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ RSpec/ExampleLength:
RSpec/NamedSubject:
Description: 'Name your RSpec subject if you reference it explicitly'
Enabled: true

RSpec/Stubject:
Description: 'Checks for stubbed test subjects'
Enabled: true
1 change: 1 addition & 0 deletions lib/rubocop-rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
require 'rubocop/cop/rspec/named_subject'
require 'rubocop/cop/rspec/nested_groups'
require 'rubocop/cop/rspec/not_to_not'
require 'rubocop/cop/rspec/stubject'
require 'rubocop/cop/rspec/verified_doubles'
92 changes: 92 additions & 0 deletions lib/rubocop/cop/rspec/stubject.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
# Checks for stubs on test subjects
#
# @example
# # bad
# describe Foo do
# subject(:bar) { baz }
#
# before do
# allow(bar).to receive(:qux?).and_return(true)
# end
# end
#
class Stubject < Cop
include RuboCop::RSpec::TopLevelDescribe, RuboCop::RSpec::Language

MSG = 'Do not stub your test subject.'.freeze

def_node_matcher :subject, <<-PATTERN
(block (send nil :subject (sym $_)) args ...)
PATTERN

def_node_matcher :anonymous_subject?, <<-PATTERN
(block (send nil :subject) args ...)
PATTERN

def_node_matcher :defines_subject?, <<-PATTERN
(block (send nil :subject (sym [!%])) args ...)
PATTERN

def_node_matcher :subject_stub, '(send nil {:allow :spy} (send nil %))'
def_node_matcher :subject_mock, <<-PATTERN
(send
(send nil :expect
(send nil %)) :to
{(send (send nil :receive ...) ...) (send nil :receive ...)})
PATTERN

def_node_matcher :example_group?, <<-PATTERN
(block
(send _ {#{ExampleGroups::ALL.to_node_pattern}} ...)
...)
PATTERN

def on_block(node)
describe, = described_constant(node)
return unless describe

find_subject(node) do |subject_name, context|
find_subject_stub(context, subject_name) do |stub|
add_offense(stub, :expression)
end
end
end

private

def find_subject_stub(context, subject_name, &block)
return if example_group?(context) && defines_subject?(context)

if subject_stub(context, subject_name) || subject_mock(context, subject_name)
yield(context)
end

context.each_child_node do |node|
find_subject_stub(node, subject_name, &block)
end
end

def defines_subject?(node)
node.each_child_node.any? do |child|
subject(child) ||
anonymous_subject?(child) ||
defines_subject?(child)
end
end

def find_subject(node, parent: nil, &block)
subject(node) { |name| yield(name, parent) }

node.each_child_node do |child|
find_subject(child, parent: node, &block)
end
end
end
end
end
end
164 changes: 164 additions & 0 deletions spec/rubocop/cop/rspec/stubject_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# frozen_string_literal: true

describe RuboCop::Cop::RSpec::Stubject do
subject(:cop) { described_class.new }

it 'complains when subject is stubbed' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
before do
allow(foo).to receive(:bar).and_return(baz)
^^^^^^^^^^ Do not stub your test subject.
end
it 'uses expect twice' do
expect(foo.bar).to eq(baz)
end
end
RUBY
end

it 'complains when subject is mocked' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
before do
expect(foo).to receive(:bar).and_return(baz)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not stub your test subject.
expect(foo).to receive(:bar)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not stub your test subject.
expect(foo).to receive(:bar).with(1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not stub your test subject.
end
it 'uses expect twice' do
expect(foo.bar).to eq(baz)
end
end
RUBY
end

it 'ignores stub within context where subject name changed' do
expect_no_violations(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'when I shake things up' do
subject(:bar) { described_class.new }
it 'tries to trick rubocop-rspec' do
allow(foo).to receive(:baz)
end
end
end
RUBY
end

it 'flags nested stubjects when nested subject uses same name' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'when I shake things up' do
subject(:foo) { described_class.new }
before do
allow(foo).to receive(:wow)
^^^^^^^^^^ Do not stub your test subject.
end
it 'tries to trick rubocop-rspec' do
expect(foo).to eql(:neat)
end
end
end
RUBY
end

it 'ignores nested stubs when nested subject is anonymous' do
expect_no_violations(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'when I shake things up' do
subject { described_class.new }
before do
allow(foo).to receive(:wow)
end
it 'tries to trick rubocop-rspec' do
expect(foo).to eql(:neat)
end
end
end
RUBY
end

it 'flags nested stubjects when example group does not define subject' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'when I shake things up' do
before do
allow(foo).to receive(:wow)
^^^^^^^^^^ Do not stub your test subject.
end
it 'tries to trick rubocop-rspec' do
expect(foo).to eql(:neat)
end
end
end
RUBY
end

it 'flags nested stubjects' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'when I shake things up' do
subject(:bar) { described_class.new }
before do
allow(foo).to receive(:wow)
allow(bar).to receive(:wow)
^^^^^^^^^^ Do not stub your test subject.
end
it 'tries to trick rubocop-rspec' do
expect(bar).to eql(foo)
end
end
end
RUBY
end

it 'flags deeply nested stubjects' do
expect_violation(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
context 'level 1' do
subject(:bar) { described_class.new }
context 'level 2' do
subject(:baz) { described_class.new }
before do
allow(foo).to receive(:wow)
allow(bar).to receive(:wow)
allow(baz).to receive(:wow)
^^^^^^^^^^ Do not stub your test subject.
end
end
end
end
RUBY
end
end

0 comments on commit d050adb

Please sign in to comment.