Class: RuboCop::Cop::RSpecParity::SufficientContexts

Inherits:
Base
  • Object
show all
Includes:
DepartmentConfig, SpecFileFinder
Defined in:
lib/rubocop/cop/rspec_parity/sufficient_contexts.rb

Overview

Ensures that specs have at least as many contexts as the method has branches.

This cop helps ensure thorough test coverage by checking that complex methods with multiple branches (if/elsif/else, case/when, &&, ||, ternary) have corresponding context blocks in their specs to test each branch.

Examples:

# bad - method has 3 branches, spec has only 1 context
# app/services/user_creator.rb
def create_user(params)
  if params[:admin]
    create_admin(params)
  elsif params[:moderator]
    create_moderator(params)
  else
    create_regular_user(params)
  end
end

# spec/services/user_creator_spec.rb
context 'when creating a user' do
  # only one context for 3 branches
end

# good - method has 3 branches, spec has 3 contexts
# spec/services/user_creator_spec.rb
context 'when creating an admin' do
end
context 'when creating a moderator' do
end
context 'when creating a regular user' do
end

Defined Under Namespace

Classes: Branch, BranchTally, CoverageGap, ParsedSpec, ScanState, SpecCoverage

Constant Summary collapse

COVERAGE_MSG =

Used when CoversAnnotations is on: names one still-uncovered branch and gives the exact context to add. Re-running advances to the next gap as branches get annotated, so the message stays short instead of listing all.

"Missing coverage for `%<token>s` (%<location>s) — " \
"%<missing>d of %<branches>d %<branch_word>s untested. " \
"Add `context '...' do # rspec_parity:covers %<token>s` to mark it covered."
MANY_MSG =

Used when many branches are missing: too many to walk one at a time, so point at the CLI that lists them all instead.

"%<missing>d of %<branches>d %<branch_word>s untested. " \
"Run `bundle exec rspec-parity-cover %<location>s` for the full list."
MANY_UNCOVERED_BRANCHES =

Above this many missing branches, switch from the per-branch message to the CLI pointer.

3
COUNT_MSG =

Used when CoversAnnotations is off (no annotation guidance to give).

"Method `%<method_name>s` is missing coverage for %<missing>d of %<branches>d %<branch_word>s."
TRACED_SUFFIX =
" (including branches from: %<traced>s)"
ORPHAN_SUFFIX =
" `rspec_parity:covers` annotation `%<label>s` matches no branch%<hint>s"
ANNOTATION_PATTERN =
/#\s*rspec_parity:covers\s+(.+?)\s*\z/
APP_DIR_PATTERN =
%r{/app/}
EXCLUDED_METHODS =
%w[initialize].freeze
EXCLUDED_PATTERNS =
[
  /^before_/,
  /^after_/,
  /^around_/,
  /^validate_/,
  /^autosave_/
].freeze
GUARD_TERMINATOR_TYPES =

Node types whose presence as a one-armed ‘if` body makes it a guard clause.

%i[return break next redo retry].freeze

Constants included from DepartmentConfig

DepartmentConfig::SHARED_CONFIG_DEFAULTS

Instance Method Summary collapse

Constructor Details

#initialize(config = nil, options = nil) ⇒ SufficientContexts

Returns a new instance of SufficientContexts.



265
266
267
268
269
270
271
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 265

def initialize(config = nil, options = nil)
  super
  @ignore_memoization = cop_config.fetch("IgnoreMemoization", true)
  @trace_single_use_private = cop_config.fetch("TraceSingleUsePrivateMethods", true)
  @covers_annotations = cop_config.fetch("CoversAnnotations", true)
  @call_graphs = {}.compare_by_identity
end

Instance Method Details

#branch_inventory_for(method_node) ⇒ Object Also known as: all_branches

Every counted branch for a method node, including those traced from single-use private helpers. Empty when the method has fewer than two branches or is excluded. Also aliased as all_branches for callers that want the full set regardless of spec coverage.



287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 287

def branch_inventory_for(method_node)
  return [] if excluded_method?(method_name(method_node))

  tally = branch_tally(method_node)
  if @trace_single_use_private
    extra = inlined_branches(method_node)
    tally += extra.branch_tally if extra.branch_tally
  end
  return [] if branches_from(tally) < 2

  branch_inventory(tally, method_node)
end

#build_gap(inventory, coverage) ⇒ Object



316
317
318
319
320
321
322
323
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 316

def build_gap(inventory, coverage)
  matched, = match_annotations(inventory, coverage.annotations)
  covered = coverage.scenarios + matched.size
  return nil if covered.zero? || covered >= inventory.size

  CoverageGap.new(uncovered: inventory.reject { |branch| matched.include?(branch) },
                  annotated: matched, unannotated_specs: coverage.scenarios)
end

#coverage_gap(method_node, spec_content) ⇒ Object



309
310
311
312
313
314
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 309

def coverage_gap(method_node, spec_content)
  inventory = branch_inventory_for(method_node)
  return nil if inventory.empty?

  build_gap(inventory, count_contexts_for_method(spec_content.to_s, method_name(method_node)))
end

#on_def(node) ⇒ Object



273
274
275
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 273

def on_def(node)
  check_method(node)
end

#on_defs(node) ⇒ Object



277
278
279
# File 'lib/rubocop/cop/rspec_parity/sufficient_contexts.rb', line 277

def on_defs(node)
  check_method(node)
end