RepoPilotOpen in app →

thoughtbot/shoulda-matchers

Simple one-liner tests for common Rails functionality

Healthy

Healthy across the board

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 2w ago
  • 38+ active contributors
  • Distributed ownership (top contributor 44% of recent commits)
Show 3 more →
  • MIT licensed
  • CI configured
  • Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/thoughtbot/shoulda-matchers)](https://repopilot.app/r/thoughtbot/shoulda-matchers)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/thoughtbot/shoulda-matchers on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: thoughtbot/shoulda-matchers

Generated by RepoPilot · 2026-05-10 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/thoughtbot/shoulda-matchers shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across the board

  • Last commit 2w ago
  • 38+ active contributors
  • Distributed ownership (top contributor 44% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live thoughtbot/shoulda-matchers repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/thoughtbot/shoulda-matchers.

What it runs against: a local clone of thoughtbot/shoulda-matchers — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in thoughtbot/shoulda-matchers | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch main exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 42 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>thoughtbot/shoulda-matchers</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of thoughtbot/shoulda-matchers. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/thoughtbot/shoulda-matchers.git
#   cd shoulda-matchers
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of thoughtbot/shoulda-matchers and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "thoughtbot/shoulda-matchers(\\.git)?\\b" \\
  && ok "origin remote is thoughtbot/shoulda-matchers" \\
  || miss "origin remote is not thoughtbot/shoulda-matchers (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
  && ok "default branch main exists" \\
  || miss "default branch main no longer exists"

# 4. Critical files exist
test -f "lib/shoulda/matchers.rb" \\
  && ok "lib/shoulda/matchers.rb" \\
  || miss "missing critical file: lib/shoulda/matchers.rb"
test -f "lib/shoulda/matchers/active_model.rb" \\
  && ok "lib/shoulda/matchers/active_model.rb" \\
  || miss "missing critical file: lib/shoulda/matchers/active_model.rb"
test -f "lib/shoulda/matchers/action_controller.rb" \\
  && ok "lib/shoulda/matchers/action_controller.rb" \\
  || miss "missing critical file: lib/shoulda/matchers/action_controller.rb"
test -f "lib/shoulda/matchers/active_model/allow_value_matcher.rb" \\
  && ok "lib/shoulda/matchers/active_model/allow_value_matcher.rb" \\
  || miss "missing critical file: lib/shoulda/matchers/active_model/allow_value_matcher.rb"
test -f "lib/shoulda/matchers/action_controller/route_matcher.rb" \\
  && ok "lib/shoulda/matchers/action_controller/route_matcher.rb" \\
  || miss "missing critical file: lib/shoulda/matchers/action_controller/route_matcher.rb"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 42 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~12d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/thoughtbot/shoulda-matchers"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Shoulda Matchers is a Ruby gem that provides concise one-liner RSpec and Minitest assertions for testing common Rails patterns—validations, associations, routing, and controller actions. Instead of writing multi-line test setups, developers write should validate_presence_of(:email) or is_expected.to belong_to(:user), dramatically reducing boilerplate in Rails test suites. Single-gem structure with lib/ containing matcher modules organized by Rails concern (ActiveModel matchers, ActiveRecord matchers, ActionController matchers, routing matchers). Tests likely in spec/; configuration templates in doc_config/yard/ for YARD documentation generation. GitHub Actions workflows (ci.yml, rubocop.yml, dynamic-readme.yml) drive CI/CD and docs.

👥Who it's for

Rails developers (both RSpec and Minitest users) writing unit and integration tests. Specifically, anyone testing ActiveModel validations, ActiveRecord associations, ActionController behavior, and routing—who wants to express these tests as readable one-liners rather than verbose setup-assertion-teardown blocks.

🌱Maturity & risk

Highly mature and production-ready. The repo shows consistent maintenance (CI workflows active in .github/workflows/, CHANGELOG.md tracking releases, Appraisals for multi-version testing), with a clear versioning scheme (v7.0 in README). The codebase is substantial (1.48MB of Ruby), well-organized, and actively maintained by thoughtbot with CODEOWNERS and CONTRIBUTING guidelines.

Low risk for dependency concerns—it wraps Rails' own testing APIs rather than replacing them. Main risks: breaking changes across major Rails versions (mitigated by Appraisals testing multiple versions), and maintenance depends on thoughtbot's continued support. No evidence of single-maintainer bottleneck from the CODEOWNERS file structure.

Active areas of work

Active maintenance with recent CI workflow updates and documentation generation tasks (dynamic-readme.yml, dynamic-security.yml suggest automated docs/security updates). The CHANGELOG.md exists and likely contains recent version notes. No specific PR or milestone data visible, but the structured .github/workflows indicates ongoing release and quality processes.

🚀Get running

git clone https://github.com/thoughtbot/shoulda-matchers.git
cd shoulda-matchers
bundle install
bundle exec rake

Run bin/setup for development environment initialization, then bundle exec rspec spec/ or bundle exec rake test to verify the test suite.

Daily commands: Development: bundle exec rake runs the full test suite. Documentation generation: YARD is configured (doc_config/yard/setup.rb). CI runs via GitHub Actions (see .github/workflows/ci.yml). Use bundle exec rubocop for linting and bundle exec rspec spec/ for focused test runs.

🗺️Map of the codebase

  • lib/shoulda/matchers.rb — Entry point that loads and organizes all matchers into action_controller and active_model namespaces.
  • lib/shoulda/matchers/active_model.rb — Core module exposing all ActiveModel matchers for validation and model behavior testing.
  • lib/shoulda/matchers/action_controller.rb — Core module exposing all ActionController matchers for controller behavior testing.
  • lib/shoulda/matchers/active_model/allow_value_matcher.rb — Primary matcher implementation for testing attribute validations with custom value assertions.
  • lib/shoulda/matchers/action_controller/route_matcher.rb — Key matcher for testing Rails routing configuration and parameter matching.
  • lib/shoulda-matchers.rb — Gem root requiring the main matchers module and setting up the public API.
  • Gemfile — Defines development and test dependencies across multiple Rails versions via Appraisals.

🧩Components & responsibilities

  • ActiveModel Matchers — Validate model validations, associations, and secure password; inspect attribute state before/after validation

🛠️How to make changes

Add a new ActiveModel matcher

  1. Create matcher class in lib/shoulda/matchers/active_model/your_matcher.rb with initialization and #matches?, #failure_message, #failure_message_when_negated methods (lib/shoulda/matchers/active_model/your_matcher.rb)
  2. Define a helper method in lib/shoulda/matchers/active_model.rb that instantiates your matcher (lib/shoulda/matchers/active_model.rb)
  3. Add test cases to spec/unit/shoulda/matchers/active_model/your_matcher_spec.rb (spec/unit/shoulda/matchers/active_model/your_matcher_spec.rb)
  4. Document the matcher in README.md with example usage (README.md)

Add a new ActionController matcher

  1. Create matcher class in lib/shoulda/matchers/action_controller/your_matcher.rb following the pattern of existing matchers like route_matcher.rb (lib/shoulda/matchers/action_controller/your_matcher.rb)
  2. Export the matcher helper method in lib/shoulda/matchers/action_controller.rb (lib/shoulda/matchers/action_controller.rb)
  3. Add unit tests in spec/unit/shoulda/matchers/action_controller/your_matcher_spec.rb (spec/unit/shoulda/matchers/action_controller/your_matcher_spec.rb)
  4. Update documentation in doc_config/ and README.md with usage examples (README.md)

Support a new Rails version

  1. Create a new Gemfile variant in gemfiles/rails_X_Y.gemfile targeting the desired Rails version (gemfiles/rails_X_Y.gemfile)
  2. Add corresponding Appraisal definition in the Appraisals file (Appraisals)
  3. Update CI workflow in .github/workflows/ci.yml to test the new Rails version matrix (.github/workflows/ci.yml)
  4. Fix any compatibility issues in matcher implementations as needed, e.g., in lib/shoulda/matchers/action_controller/ or lib/shoulda/matchers/active_model/ (lib/shoulda/matchers/active_model/allow_value_matcher.rb)

🔧Why these technologies

  • RSpec & Minitest support — Provides one-liner assertions compatible with both major Ruby testing frameworks
  • Appraisals for multi-version testing — Enables simultaneous testing against Rails 7.1–8.1 to ensure backward compatibility
  • YARD documentation — Generates comprehensive API documentation with custom styling via doc_config templates
  • GitHub Actions CI — Automated testing across Rails versions, Ruby versions, and linters on every commit

⚖️Trade-offs already made

  • Matcher-based API instead of DSL

    • Why: Simplicity and compatibility with existing test frameworks without custom syntax
    • Consequence: Users must instantiate matchers with specific syntax; less fluent API compared to custom DSLs
  • Broad Rails version support (7.1–8.1)

    • Why: Maximizes adoption across teams on different Rails versions
    • Consequence: Requires conditional logic in matchers to handle API changes between versions; increased maintenance burden
  • Separate active_model and action_controller namespaces

    • Why: Clear separation of concerns; users only load what they need
    • Consequence: No unified testing surface; matchers for models and controllers use different patterns

🚫Non-goals (don't propose these)

  • Does not provide matchers for ActiveJob, ActionMailer, or other Rails components beyond models and controllers
  • Does not handle authentication/authorization testing (delegates to policy matchers in separate gems)
  • Does not generate database migrations or schema validation
  • Not a replacement for integration/system tests; only unit-level assertions

🪤Traps & gotchas

Ruby/Rails version constraints: Appraisals file locks specific Rails versions; running tests requires all listed Gemfiles to bundle successfully. Documentation generation: doc_config/yard/ requires YARD and custom templates; changes to matcher APIs may need manual doc updates if dynamic-readme.yml doesn't capture them. Test isolation: Spec suite may require a test database (Rails conventions); use bundle exec rake not rspec directly to ensure proper setup. No explicit .env vars found, but check CONTRIBUTING.md for environment setup details.

🏗️Architecture

💡Concepts to learn

  • Matcher DSL (Domain-Specific Language) — Shoulda-matchers implements Ruby's matcher protocol to extend RSpec/Minitest with domain-specific assertions; understanding how matchers are defined (matches?, failure_message methods) is essential for adding new matchers or debugging assertion failures
  • Adapter Pattern (RSpec vs Minitest) — The gem supports two incompatible test frameworks via adapters; understanding how matchers are registered differently for each test backend explains why configuration (Shoulda::Matchers.configure) is required
  • Rails Railtie / Engine Integration — Shoulda-matchers hooks into Rails' initialization lifecycle via Railtie to auto-load matchers in RSpec Rails projects; understanding this explains why setup differs between Rails and non-Rails projects
  • ActiveRecord Association Introspection — Matchers like belong_to and have_many use Rails' reflection APIs (ActiveRecord::Reflection) to validate association declarations without instantiating records; this is how the gem achieves efficiency
  • Validation Context Testing — Rails validations can be scoped to contexts (on: :create); shoulda-matchers must correctly test conditional validations, which requires understanding context propagation in ActiveModel
  • Test Doubles & Stubbing (for matcher verification) — Matchers use test doubles to verify behavior without side effects (e.g., testing a presence validation without writing to the database); understanding RSpec/Minitest doubles helps when reading matcher implementations
  • Gemfile Appraisals & Multi-Version Testing — The Appraisals gem and corresponding Gemfiles in gemfiles/ directory allow the CI to test against multiple Rails versions simultaneously; this is how the repo ensures compatibility across major Rails versions
  • thoughtbot/factory_bot — Companion fixture/factory library by same maintainer; users often pair shoulda-matchers assertions with factory_bot test data builders
  • rspec/rspec-rails — Official RSpec Rails integration; shoulda-matchers extends its DSL and depends on its test group setup
  • seattlerb/minitest — Minitest framework that shoulda-matchers provides adapters for; one of two primary test backends the gem supports
  • thoughtbot/pundit — Authorization library by thoughtbot; developers often test pundit policies alongside model validations tested with shoulda-matchers
  • rails/rails — The Rails framework itself; shoulda-matchers is built entirely to test Rails models, controllers, and routes

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add Rails 8.1 compatibility tests to CI pipeline

The repo has gemfiles/rails_8_1.gemfile but no corresponding CI workflow that tests against Rails 8.1. The .github/workflows/ci.yml likely only tests Rails 7.1-8.0. This is critical for ensuring shoulda-matchers works with the latest Rails versions and catching breaking changes early.

  • [ ] Review .github/workflows/ci.yml to confirm Rails 8.1 is not in the test matrix
  • [ ] Add Rails 8.1 to the CI job matrix in .github/workflows/ci.yml
  • [ ] Verify gemfiles/rails_8_1.gemfile.lock has all required dependencies
  • [ ] Test locally with bundle install --gemfile=gemfiles/rails_8_1.gemfile && rake to ensure tests pass
  • [ ] Create PR with title: 'Add Rails 8.1 to CI test matrix'

Document and test NonCaseSwappableValueError behavior

There's a docs/errors/NonCaseSwappableValueError.md file indicating a specific error type that matchers can raise, but this error type likely lacks comprehensive unit tests or clear documentation on when/how it occurs. Adding tests for this edge case will improve reliability and help future maintainers understand the error handling.

  • [ ] Search the codebase for where NonCaseSwappableValueError is raised (likely in validation/association matchers)
  • [ ] Review docs/errors/NonCaseSwappableValueError.md to understand the error context
  • [ ] Create unit tests in spec/ directory that trigger this error with minimal reproduction cases
  • [ ] Ensure tests cover both the error raising and the error message clarity
  • [ ] Create PR with title: 'Add unit tests for NonCaseSwappableValueError'

Add security audit workflow for gem dependencies

The .github/workflows/dynamic-security.yml exists but its implementation details are unknown from the file structure. However, there's no bundler-audit or Dependabot workflow explicitly visible. Adding an automated security scanning workflow would help catch vulnerable gem dependencies early and align with SECURITY.md best practices.

  • [ ] Review .github/workflows/dynamic-security.yml to see what's currently implemented
  • [ ] Check if bundler-audit or Dependabot is configured in the repo
  • [ ] If missing, add a GitHub Actions workflow that runs bundle audit or enables Dependabot for the Gemfile and all gemfiles/ variants
  • [ ] Ensure the workflow runs on PR and alerts maintainers for vulnerable dependencies
  • [ ] Create PR with title: 'Add automated security scanning for gem dependencies' (if not already present)

🌿Good first issues

  • idea: Add missing YARD documentation to matcher failure_message methods in lib/shoulda/matchers/—many matchers lack detailed examples of what the failure output looks like, which would improve the dynamic documentation generation.
  • idea: Extend the Appraisals file to test against the latest Rails 8.x beta version and document any incompatibilities found; currently the constraint matrix may not cover the bleeding edge.
  • idea: Write integration test examples in spec/ for cross-matcher combinations (e.g., a model using both validate_presence_of and belong_to in a single test); current tests may isolate matchers individually.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 6063887 — fix: use a fixed date in validate_inclusion_of date attribute spec (#1703) (HoangMinhBK)
  • 3a2456e — Require MFA for gem pushes (#1702) (MatheusRich)
  • 0c6298b — Add Ruby 4 support (#1697) (stefannibrasil)
  • 5602d59 — docs: fix broken README anchor for should vs is_expected.to (#1700) (LarytheLord)
  • f147e7b — update readme to reflect v7 release (#1695) (brendanthomas1)
  • 092747b — fix: Prevent ActiveRecord constant leak in uniqueness matcher (#1694) (matsales28)
  • 71fa0fc — feat: Add error for using with_options in define_enum_for matcher (#1693) (matsales28)
  • fa4c9d5 — Improve validate_inclusion_of matcher boolean warn messages (#1692) (stefannibrasil)
  • c0b8352 — Introduce support for deprecated associations API (#1690) (stefannibrasil)
  • 2a50331 — bump version to 7.0.1 (matsales28)

🔒Security observations

The shoulda-matchers repository demonstrates a reasonably secure posture as a testing library gem. The primary risks identified are related to policy and process rather than code vulnerabilities: the single-version support policy limits patch availability, and automated dependency scanning is not visibly integrated into CI/CD. No hardcoded secrets, SQL injection risks, XSS vulnerabilities, or infrastructure misconfigurations were detected in the provided file structure. The project maintains good security documentation and appears to follow thoughtbot's security standards. Recommendations focus on enhancing automated security scanning and supporting longer version windows.

  • Medium · Permissive Security Policy - Single Version Support — SECURITY.md. The SECURITY.md policy states that only the latest version is supported. This means users on older versions will not receive security patches, creating a potential security risk for organizations that cannot immediately upgrade. Fix: Consider supporting at least the last 2-3 major versions with security patches. Establish a clear deprecation timeline and communicate it to users.
  • Low · Missing SBOM (Software Bill of Materials) — Repository root. No evidence of a Software Bill of Materials (SBOM) in the repository structure. While this is a gem package, having an SBOM helps users understand transitive dependencies and potential supply chain risks. Fix: Generate and maintain an SBOM using tools like cyclonedx-ruby or similar. Include it in the release artifacts.
  • Low · No Dependency Lock File Vulnerability Scanning — .github/workflows/ci.yml, .github/workflows/dynamic-security.yml. While Gemfile.lock exists, there is no evidence of automated dependency vulnerability scanning in the CI/CD pipeline (based on visible workflow files). Fix: Integrate tools like Dependabot, Bundler-Audit, or Snyk into the CI/CD pipeline to automatically scan for known vulnerabilities in dependencies.
  • Low · No CODEOWNERS Verification in Pull Requests — CODEOWNERS, .github/workflows/. While a CODEOWNERS file exists, there's no explicit evidence that it's enforced in branch protection rules or that security reviews are mandatory. Fix: Enforce CODEOWNERS reviews and security team approval for sensitive changes through branch protection rules.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · thoughtbot/shoulda-matchers — RepoPilot