File: subject_stub.rb

package info (click to toggle)
ruby-rubocop-rspec 2.16.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,892 kB
  • sloc: ruby: 22,283; makefile: 4
file content (156 lines) | stat: -rw-r--r-- 5,036 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# frozen_string_literal: true

require 'set'

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://penelope.zone/2015/12/27/introducing-rspec-smells-and-where-to-find-them.html#smell-1-stubjec
      # @see https://github.com/rubocop-hq/rspec-style-guide#dont-stub-subject
      #
      # @example
      #   # bad
      #   describe Article 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
      #
      #   # 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
      #       expect(article.description).to include('by an unknown author')
      #     end
      #   end
      #
      class SubjectStub < Base
        include TopLevelGroup

        MSG = 'Do not stub methods of the object under test.'

        # @!method subject?(node)
        #   Find a named or unnamed subject definition
        #
        #   @example anonymous subject
        #     subject?(parse('subject { foo }').ast) do |name|
        #       name # => :subject
        #     end
        #
        #   @example named subject
        #     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?
              { #Subjects.all (sym $_) | $#Subjects.all }
            ) 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)
        #     allow(foo).to  receive(:bar).with(1)
        #     allow(foo).to  receive(:bar).with(1).and_return(2)
        #     expect(foo).to receive(:bar)
        #     expect(foo).to receive(:bar).with(1)
        #     expect(foo).to receive(:bar).with(1).and_return(2)
        #
        def_node_matcher :message_expectation?, <<-PATTERN
          (send
            {
              (send nil? { :expect :allow } (send nil? %))
              (send nil? :is_expected)
            }
            #Runners.all
            #message_expectation_matcher?
          )
        PATTERN

        # @!method message_expectation_matcher?(node)
        def_node_search :message_expectation_matcher?, <<-PATTERN
          (send nil? {
            :receive :receive_messages :receive_message_chain :have_received
            } ...)
        PATTERN

        def on_top_level_group(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(node)
          node.each_descendant(:block).with_object({}) do |child, h|
            name = yield(child)
            next unless name

            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 = [*subject_names, *@explicit_subjects[node]]
          subject_names -= @subject_overrides[node] if @subject_overrides[node]

          names = Set[*subject_names, :subject]
          expectation_detected = message_expectation?(node, names)
          return yield(node) if expectation_detected

          node.each_child_node(:send, :def, :block, :begin) do |child|
            find_subject_expectations(child, subject_names, &block)
          end
        end
      end
    end
  end
end