Mutante

Find dead code by commenting lines out and running the tests. If the suite still passes, that line is a strong candidate for removal.

Think of it as reverse mutation testing: instead of mutating code to see if the suite catches the change, mutante removes code to see if anything notices.

Installation

Add to your Gemfile:

group :development, :test do
  gem "mutante"
end

Then:

bundle install

Usage

Run against every candidate file in a Rails project (models, controllers, services, jobs, mailers, helpers, channels, serializers, and lib/):

bundle exec mutante

Limit to a single file or directory:

bundle exec mutante app/models/user.rb
bundle exec mutante app/services/

Stream test output while it runs:

bundle exec mutante --verbose

Mutante starts by running the whole suite. If the baseline is red, it bails out — there's no signal in running mutated tests against an already-broken suite.

For every line of real code it then:

  1. Rewrites the file with that one line commented out.
  2. Skips the line if the mutation produces a SyntaxError (e.g. end, def foo, block headers).
  3. Runs the spec file that covers the source file. If no match is configured and no mirrored spec exists, runs the entire suite.
  4. Restores the file, and flags the line if the suite stayed green.

At the end you get a list of flagged path:line locations to review.

Configuration

Drop a file at config/initializers/mutante.rb (or .mutante.rb in the project root):

Mutante.configure do |config|
  # Override the test command (default: bin/rspec or bundle exec rspec).
  config.test_command = "bundle exec rspec --no-color"

  # Narrow or widen which files are scanned.
  config.include_globs = %w[app/services/**/*.rb app/models/**/*.rb]
  config.exclude_globs << "app/services/legacy/**/*"

  # Map a source-file glob to the spec(s) that cover it. Available
  # placeholders in the target:
  #   {basename}     - file name without extension
  #   {relative}     - path below app/<layer>/ or lib/
  #   {relative_dir} - directory portion of {relative}
  config.map "app/services/**/*.rb",
             to: "spec/services/{relative}_spec.rb"

  config.map "app/models/concerns/*.rb",
             to: ["spec/models/concerns/{basename}_spec.rb",
                  "spec/shared/concerns/{basename}_spec.rb"]
end

If no mapping matches, mutante falls back to the mirrored path under spec/ (so app/models/user.rbspec/models/user_spec.rb). If that doesn't exist either, it runs the full suite for that line.

Performance

This is slow. Commenting out a single line means a full test run, and mutante does that once per eligible line. Use specific files or tight mappings to keep runs tractable on large codebases.

License

MIT.