Class: RuboCop::Cop::RSpecGuide::MinimumBehavioralCoverage

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

Overview

Checks that describe blocks test at least 2 behavioral variations.

Testing only a single scenario (happy path OR edge case) provides insufficient coverage. Tests should verify both expected behavior and edge case handling to ensure comprehensive validation.

This can be achieved in two ways:

  1. Use 2+ sibling context blocks (happy path + edge cases)
  2. Combine it-blocks (default behavior) with context-blocks (edge cases)

Examples:

Traditional approach - 2+ sibling contexts

# bad - only one scenario (no edge case testing)
describe '#calculate' do
  context 'with valid data' do
    it { expect(result).to eq(100) }
  end
end

# good - multiple scenarios (happy path + edge cases)
describe '#calculate' do
  context 'with valid data' do
    it { expect(result).to eq(100) }
  end

  context 'with invalid data' do
    it { expect { result }.to raise_error(ValidationError) }
  end
end

# good - more comprehensive coverage
describe '#calculate' do
  context 'with positive numbers' do
    it { expect(result).to eq(100) }
  end

  context 'with zero' do
    it { expect(result).to eq(0) }
  end

  context 'with negative numbers' do
    it { expect { result }.to raise_error(ArgumentError) }
  end
end

New pattern - it-blocks + context-blocks

# bad - only default behavior, no edge cases
describe '#calculate' do
  it 'calculates sum' { expect(result).to eq(100) }
end

# good - default behavior + edge case
describe '#calculate' do
  it 'calculates sum with defaults' { expect(result).to eq(100) }

  context 'with invalid input' do
    it { expect { result }.to raise_error(ValidationError) }
  end
end

# good - multiple it-blocks for defaults, context for edge case
describe '#calculate' do
  it 'returns numeric result' { expect(result).to be_a(Numeric) }
  it 'is positive' { expect(result).to be > 0 }

  context 'with special conditions' do
    it { expect(result).to eq(0) }
  end
end

Edge case - setup before tests (allowed)

# good - setup + it-blocks + contexts
describe '#calculate' do
  let(:calculator) { Calculator.new }
  before { calculator.configure }

  it 'works with defaults' { expect(result).to eq(100) }

  context 'with custom config' do
    it { expect(result).to eq(200) }
  end
end

When to disable this cop

# Simple getter with no edge cases - disable is acceptable
describe '#name' do # rubocop:disable RSpecGuide/MinimumBehavioralCoverage
  it { expect(subject.name).to eq('test') }
end

Direct Known Subclasses

CharacteristicsAndContexts

Constant Summary collapse

MSG =
"Describe block should test at least 2 behavioral variations: " \
"either use 2+ sibling contexts (happy path + edge cases), " \
"or combine it-blocks for default behavior with context-blocks for edge cases. " \
"Use `# rubocop:disable RSpecGuide/MinimumBehavioralCoverage` " \
"for simple cases (e.g., getters) with no edge cases."

Instance Method Summary collapse

Instance Method Details

#context_only?(node) ⇒ Object



111
112
113
# File 'lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb', line 111

def_node_matcher :context_only?, "(block (send nil? :context ...) ...)\n"

#on_block(node) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb', line 115

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

  children = collect_children(node)
  contexts = children.select { |child| context_only?(child) }
  its = children.select { |child| example?(child) }

  # Valid if: 2+ contexts OR (1+ it-blocks before contexts AND 1+ contexts)
  return if contexts.size >= 2
  return if valid_it_then_context_pattern?(children, its, contexts)

  add_offense(node)
end