Class: RuboCop::Cop::RSpecGuide::HappyPathFirst

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

Overview

Checks that corner cases are not the first context in a describe block.

Placing happy path scenarios first improves test readability by establishing the expected behavior before diving into edge cases. This makes it easier for readers to understand the primary purpose of the code being tested.

The cop allows corner case contexts to appear first if there are example blocks (it/specify) before the first context, as those examples represent the happy path.

Examples:

Bad - corner case first

# bad - starts with negative case
describe '#process' do
  context 'but user is blocked' do
    it { expect { process }.to raise_error }
  end

  context 'when user is valid' do
    it { expect(process).to be_success }
  end
end

# bad - starts with NOT condition
describe '#activate' do
  context 'when user does NOT exist' do
    it { expect { activate }.to raise_error(NotFound) }
  end

  context 'when user exists' do
    it { expect(activate).to be_truthy }
  end
end

Good - happy path first

# good - happy path comes first
describe '#subscribe' do
  context 'with valid card' do
    it { expect(subscribe).to be_success }
  end

  context 'but payment fails' do
    it { expect(subscribe).to be_failure }
  end
end

# good - positive case before negative
describe '#send_notification' do
  context 'when user has email' do
    it { expect(send_notification).to be_sent }
  end

  context 'without email' do
    it { expect(send_notification).to be_skipped }
  end
end

Edge case - it-blocks represent happy path

# good - examples before first context represent happy path
describe '#add_child' do
  it 'adds child to children collection' do
    expect { add_child(child) }.to change(parent.children, :count).by(1)
  end

  context 'but child is already in collection' do
    it { expect { add_child(child) }.not_to change(parent.children, :count) }
  end
end

# good - multiple it-blocks as happy path
describe '#calculate' do
  it { expect(calculate).to be_a(Numeric) }
  it { expect(calculate).to be_positive }

  context 'with invalid input' do
    it { expect { calculate }.to raise_error }
  end
end

Constant Summary collapse

MSG =
"Place happy path contexts before corner cases. " \
"First context appears to be a corner case: %<description>s"
CORNER_CASE_WORDS =

Words indicating corner cases

%w[
  error failure invalid suspended blocked denied
  fails missing absent unavailable
].freeze

Instance Method Summary collapse

Instance Method Details

#context_with_description?(node) ⇒ Object



102
103
104
105
106
# File 'lib/rubocop/cop/rspec_guide/happy_path_first.rb', line 102

def_node_matcher :context_with_description?, "(block\n  (send nil? :context (str $_description) ...)\n  ...)\n"

#on_block(node) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/rubocop/cop/rspec_guide/happy_path_first.rb', line 108

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

  contexts = collect_direct_child_contexts(node)
  return if contexts.size < 2

  # If there are any examples (it/specify) before the first context,
  # this is a happy path, so no offense
  return if has_examples_before_first_context?(node, contexts.first)

  # Check first context
  context_with_description?(contexts.first) do |description|
    if corner_case_context?(description)
      add_offense(
        contexts.first,
        message: format(MSG, description: description)
      )
    end
  end
end