Skip to content

ydah/rspec-covers

Repository files navigation

rspec-covers

CI Gem Version

rspec-covers adds coverage intent metadata to RSpec examples.

It lets a spec declare the production code it is responsible for with covers, declare allowed dependencies with uses, and optionally fail examples that execute production code outside that declaration.

Installation

Add the gem to your application's Gemfile:

gem "rspec-covers"

Usage

Configure the integration from spec_helper.rb:

require "rspec/covers"

RSpec.configure do |config|
  RSpec::Covers.configure(config) do |covers|
    covers.strict = true
    covers.risky = :fail
    covers.mode = :executed
    covers.undeclared = :warn
    covers.production_paths = %w[app lib]
    covers.allowlist = %w[app/models/application_record.rb lib/instrumentation/**]
    covers.suggest = true
    covers.validate_declarations = true
    covers.report_path = ".rspec-covers/result.json"
  end
end

Declare intent on example groups or examples:

RSpec.describe UserCreator, covers: UserCreator do
  it "creates a user", covers: "UserCreator#call" do
    expect(UserCreator.new(params).call).to be_persisted
  end

  it "notifies", covers: "UserCreator#call", uses: [Notifier, "User.create!"] do
    expect(UserCreator.new(params).call).to be_success
  end
end

covers and uses accept:

  • constants such as UserCreator
  • method strings such as "UserCreator#call" or "User.create!"
  • file paths or globs such as "app/services/**/*.rb"
  • regular expressions matched against production method labels and paths

Strict Coverage

For each example, rspec-covers records per-example line coverage with Coverage.peek_result.

When strict is enabled, an example is risky if it executes production lines that are not covered by its covers or uses declaration. Risky examples raise RSpec::Covers::StrictCoverageError by default.

Set covers.risky = :report to keep the RSpec example green while still recording the example as risky in formatter output and JSON reports.

Examples with no declaration follow covers.undeclared:

  • :ignore ignores them
  • :warn prints a warning
  • :risky fails when production code is executed

Suggestions

With covers.suggest = true, undeclared examples receive ranked covers suggestions. The scorer combines:

  • NC: spec file or described class naming convention
  • NCC: token overlap between example text and method labels
  • LCBA: the production call immediately before expect(...)
  • dynamic: suite-wide tf-idf over production calls

Run:

rake covers:suggest[.rspec-covers/result.json]

With covers.validate_declarations = true, examples that already declare covers are also scored. Weakly supported declarations are written to declaration_validation in the JSON report and shown by the formatter.

Checked Mode

covers.mode = :checked narrows executed coverage to production methods that contributed to expectations. It tracks production method return values passed to expect(...) and the last production call before each expectation. This is an approximation of checked coverage, not a full dynamic slice.

In checked mode, the JSON report includes unchecked_locations: production lines that ran during the example but were not attributed to expectation inputs.

Rake Tasks

rake covers:report[.rspec-covers/result.json]
rake covers:suggest[.rspec-covers/result.json]
rake covers:evaluate[ground_truth.json,predictions.json]

The JSON report also includes orphan_methods: production methods found from loaded constants and static Ripper scans of production_paths that were not named by any example's covers declaration.

covers:evaluate reports traceability precision, recall, F1, and applicability for suggestions against a JSON ground-truth file. If the ground truth examples include risky, seeded, or checked_locations, it also reports strict coverage, seeded strict recall, and checked coverage metrics. Targets can be declared in the ground truth under targets and are reported as pass/fail comparisons.

Ground truth can be a simple mapping:

{
  "examples": {
    "spec/example_spec.rb[1:1]": ["UserCreator#call"]
  }
}

Or an array with strict and checked labels:

{
  "targets": {
    "traceability.f1": 0.75,
    "strict.recall": 0.90,
    "checked.f1": 0.70
  },
  "examples": [
    {
      "id": "spec/example_spec.rb[1:1]",
      "covers": ["UserCreator#call"],
      "risky": true,
      "seeded": true,
      "checked_locations": [{ "file": "app/user_creator.rb", "line": 12 }]
    }
  ]
}

Enable JSON formatter events with:

covers.json_events = true

When RSpec::Covers::Formatter is active, it emits JSON Lines with type: "rspec_covers.example" for each recorded example.

Minitest

The core coverage engine can also be attached to Minitest:

require "minitest/covers"

Minitest::Covers.configure do |covers|
  covers.strict = true
end

class UserCreatorTest < Minitest::Test
  covers "UserCreator#call"
  uses "Notifier.deliver", target: :test_creates_user

  def test_creates_user
    assert UserCreator.new(params).call
  end
end

Class-level covers / uses apply to all tests in the class. Pass target: :test_name for method-level Minitest metadata. In checked mode, Minitest assert(value) records value as the expectation actual.

Notes

Line ranges use RubyVM::AbstractSyntaxTree on CRuby and fall back to method_source when the VM does not expose AST ranges. Coverage from threads created inside an example belongs to the same process and may appear in that example's evidence.

If SimpleCov or another tool has already started Ruby coverage, rspec-covers uses Coverage.peek_result deltas and does not restart coverage.

About

RSpec coverage intent metadata, strict coverage checks, and traceability suggestions.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages