Class: RuboCop::Cop::RSpecGuide::ContextSetup

Inherits:
RuboCop::Cop::RSpec::Base
  • Object
show all
Defined in:
lib/rubocop/cop/rspec_guide/context_setup.rb

Overview

Checks that context blocks have setup (let/before) to distinguish them from the parent context.

Contexts exist to test different scenarios or states. Without explicit setup, the context doesn't actually change anything from its parent, making the context boundary meaningless and confusing.

Valid setup methods: let, let!, let_it_be, let_it_be!, before

Note: subject should be defined at describe level, not in contexts, as it describes the object under test, not context-specific state. Use RSpec/LeadingSubject cop to ensure subject is defined first.

Examples:

Bad - no setup

# bad - context has no setup, so what's different?
context 'when user is premium' do
  it { expect(user).to have_access }
end

# bad - subject in context (should be in describe)
context 'with custom config' do
  subject { Calculator.new(config) }  # Wrong place!
  it { is_expected.to be_valid }
end

Good - using let

# good - let defines context-specific state
context 'when user is premium' do
  let(:user) { create(:user, :premium) }
  it { expect(user).to have_access }
end

# good - let! for immediate evaluation
context 'with existing records' do
  let!(:records) { create_list(:record, 3) }
  it { expect(Record.count).to eq(3) }
end

Good - using let_it_be (test-prof/rspec-rails)

# good - let_it_be for performance (created once per context)
context 'with many users' do
  let_it_be(:users) { create_list(:user, 100) }
  it { expect(users.size).to eq(100) }
end

# good - let_it_be! for immediate evaluation
context 'with frozen time' do
  let_it_be!(:timestamp) { Time.current }
  it { expect(timestamp).to be_frozen }
end

Good - using before

# good - before modifies existing state
context 'when user is upgraded' do
  before { user.upgrade_to_premium! }
  it { expect(user).to be_premium }
end

# good - before with multiple setup steps
context 'with configured environment' do
  before do
    allow(ENV).to receive(:[]).with('API_KEY').and_return('test-key')
    allow(ENV).to receive(:[]).with('API_URL').and_return('http://test')
  end
  it { expect(api_client).to be_configured }
end

Subject placement

# bad - subject in context
describe Calculator do
  context 'with custom config' do
    subject { Calculator.new(custom_config) }  # Wrong!
    it { is_expected.to be_valid }
  end
end

# good - subject in describe, config in context
describe Calculator do
  subject { Calculator.new(config) }

  context 'with custom config' do
    let(:config) { CustomConfig.new }  # Right!
    it { is_expected.to be_valid }
  end
end

Constant Summary collapse

MSG =
"Context should have setup (let/let!/let_it_be/let_it_be!/before) to distinguish it from parent context"

Instance Method Summary collapse

Instance Method Details

#context_only?(node) ⇒ Object



103
104
105
# File 'lib/rubocop/cop/rspec_guide/context_setup.rb', line 103

def_node_matcher :context_only?, <<~PATTERN
  (block (send nil? :context ...) ...)
PATTERN

#on_block(node) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rubocop/cop/rspec_guide/context_setup.rb', line 107

def on_block(node)
  # Fast pre-check: only process context blocks
  return unless node.method?(:context)
  return unless context_only?(node)

  # Check if context has at least one setup node (let or before)
  # Note: subject is NOT counted as context setup because it describes
  # the object under test, not context-specific state
  has_setup = has_context_setup?(node)

  add_offense(node) unless has_setup
end