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:
- Rewrites the file with that one line commented out.
- Skips the line if the mutation produces a
SyntaxError(e.g.end,def foo, block headers). - Runs the spec file that covers the source file. If no match is configured and no mirrored spec exists, runs the entire suite.
- 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.rb → spec/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.