thoughtbot/shoulda
Makes tests easy on the fingers and the eyes
Healthy across all four use cases
Permissive license, no critical CVEs, actively maintained — safe to depend on.
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓Last commit 12mo ago
- ✓24+ active contributors
- ✓Distributed ownership (top contributor 22% of recent commits)
Show 4 more →Show less
- ✓MIT licensed
- ✓CI configured
- ✓Tests present
- ⚠Slowing — last commit 12mo ago
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.
[](https://repopilot.app/r/thoughtbot/shoulda)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 on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: thoughtbot/shoulda
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:
- 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. - 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.
- 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 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 all four use cases
- Last commit 12mo ago
- 24+ active contributors
- Distributed ownership (top contributor 22% of recent commits)
- MIT licensed
- CI configured
- Tests present
- ⚠ Slowing — last commit 12mo ago
<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
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/thoughtbot/shoulda.
What it runs against: a local clone of thoughtbot/shoulda — 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 | 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 ≤ 381 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of thoughtbot/shoulda. If you don't
# have one yet, run these first:
#
# git clone https://github.com/thoughtbot/shoulda.git
# cd shoulda
#
# 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 and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "thoughtbot/shoulda(\\.git)?\\b" \\
&& ok "origin remote is thoughtbot/shoulda" \\
|| miss "origin remote is not thoughtbot/shoulda (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.rb" \\
&& ok "lib/shoulda.rb" \\
|| miss "missing critical file: lib/shoulda.rb"
test -f "shoulda.gemspec" \\
&& ok "shoulda.gemspec" \\
|| miss "missing critical file: shoulda.gemspec"
test -f "lib/shoulda/version.rb" \\
&& ok "lib/shoulda/version.rb" \\
|| miss "missing critical file: lib/shoulda/version.rb"
test -f "test/acceptance_test_helper.rb" \\
&& ok "test/acceptance_test_helper.rb" \\
|| miss "missing critical file: test/acceptance_test_helper.rb"
test -f "MAINTAINING.md" \\
&& ok "MAINTAINING.md" \\
|| miss "missing critical file: MAINTAINING.md"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 381 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~351d)"
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"
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).
⚡TL;DR
Shoulda is an umbrella gem that bundles Shoulda Context and Shoulda Matchers to make writing Rails-specific tests more readable and maintainable under Minitest and Test::Unit. It provides expressive DSL syntax for defining test contexts, assertion matchers for model validations/associations, and custom assertions that read like documentation (e.g., should validate_presence_of(:email), should have_many(:posts)). Ultra-minimal umbrella structure: lib/shoulda.rb is the entry point that pulls in dependencies; lib/shoulda/version.rb tracks version; test/ contains acceptance tests (test/acceptance/) that verify integration with Rails, plus support helpers for running tests against multiple Rails versions (gemfiles/rails_6_1.gemfile, rails_7_0.gemfile). CI is multi-appraisal testing via GitHub Actions.
👥Who it's for
Ruby/Rails developers writing tests in Minitest or Test::Unit who want to reduce boilerplate test code and write assertions that read naturally. Maintainers of Rails applications that prioritize test readability and long-term test maintainability over speed of initial test writing.
🌱Maturity & risk
Shoulda is mature and stable—it's maintained by thoughtbot (a well-respected agency) with active CI via GitHub Actions (.github/workflows/ci.yml) and semantic versioning discipline. The project is actively maintained (current maintainer Pedro Paiva, previous long list of maintainers), tested across Ruby 3.0+/Rails 6.1+/RSpec 3.x/Minitest 4.x, and has a changelog (CHANGELOG.md) tracking releases. Verdict: production-ready, actively maintained.
Low risk overall, but worth noting: Shoulda is an umbrella gem with minimal code of its own (mostly just requires shoulda-context and shoulda-matchers), so its stability depends heavily on upstream gems. The project is currently maintained by one person (Pedro Paiva) creating single-maintainer risk. No major breaking changes are visible in recent history, and the gem targets Ruby 3.0+ only (drops support for Ruby < 3.0 as of v5).
Active areas of work
The repo is in maintenance mode—no major feature work visible. The dynamic-readme.yml and dynamic-security.yml workflows suggest automated documentation and security scanning. Appraisal gemfiles exist for Rails 6.1 and 7.0, indicating ongoing compatibility testing. The MAINTAINING.md file suggests this is a well-documented maintenance effort.
🚀Get running
git clone https://github.com/thoughtbot/shoulda.git
cd shoulda
bundle install
bundle exec rake test
Daily commands:
Run all tests: bundle exec rake test. Run tests for specific Rails version: bundle exec appraisal rails_7_0 bundle exec rake test (after script/install_gems_in_all_appraisals). Linting: bundle exec rubocop. See Rakefile for all available tasks.
🗺️Map of the codebase
lib/shoulda.rb— Entry point that loads and coordinates the two umbrella gems (Shoulda Context and Shoulda Matchers).shoulda.gemspec— Defines gem metadata and dependencies on shoulda-context and shoulda-matchers; critical for understanding what this umbrella gem provides.lib/shoulda/version.rb— Single source of truth for the gem version used across the package.test/acceptance_test_helper.rb— Sets up the acceptance test environment and fixtures; essential for understanding how the gem is tested end-to-end.MAINTAINING.md— Documents maintenance practices, release procedures, and contributor guidelines for this umbrella gem.Rakefile— Defines build tasks including test execution and gem packaging for the project.
🧩Components & responsibilities
- lib/shoulda.rb (Ruby require system) — Single entry point that requires both shoulda-context and shoulda-matchers to make them available to users.
- Failure mode: If either dependency fails to load, the entire gem fails to initialize and tests cannot run.
- Shoulda Context (external dependency) (Minitest/Test::Unit hooks) — Provides context/should DSL for organizing tests into nested groups with setup/teardown.
- Failure mode: Context blocks fail to execute if shoulda-context gem is not installed or incompatible.
- Shoulda Matchers (external dependency) (Rails ActiveModel/ActiveRecord introspection) — Provides chainable assertion matchers for validating Rails models, controllers, associations, etc.
- Failure mode: Matchers cannot introspect Rails objects if Rails or shoulda-matchers is not properly loaded.
- Acceptance test suite (Snowglobe, Rails, Minitest) — Verifies that Shoulda integrates correctly with real Rails applications across multiple versions.
- Failure mode: If integration breaks, acceptance tests fail; prevents releasing broken versions.
🔀Data flow
Developer (Gemfile)→shoulda.gemspec— Developer declares shoulda gem; gemspec resolves shoulda-context and shoulda-matchersshoulda.gemspec→lib/shoulda.rb— Gemspec requires lib/shoulda.rb at load timelib/shoulda.rb→Shoulda Context + Shoulda Matchers (external gems)— Loads and exposes both sub-gems' functionalityTest file (using shoulda)→Minitest test runner— User's test code invokes context/should blocks and matchers; Minitest executes themCI system (Appraisals)→gemfiles/*— CI reads appraisal gemfiles to install specific Rails versions for matrix testing
🛠️How to make changes
Add support for a new Rails version
- Create a new appraisal gemfile in gemfiles/ directory (e.g., rails_8_0.gemfile) specifying the new Rails version (
gemfiles/rails_7_0.gemfile) - Update Appraisals file to include the new Rails version (
Appraisals) - Generate lockfile by running bundle exec appraisal install (
gemfiles/rails_7_0.gemfile.lock) - Update CI workflow to test against the new Rails version (
.github/workflows/ci.yml) - Run acceptance tests to verify integration with new Rails version (
test/acceptance/integrates_with_rails_test.rb)
Update dependencies in all appraisal gemfiles
- Run the provided script to update gems across all gemfiles (
script/update_gems_in_all_appraisals) - Verify all gemfile locks are properly regenerated (
gemfiles/rails_7_0.gemfile.lock) - Run acceptance test suite to ensure no breakage (
test/acceptance_test_helper.rb)
Cut a new release
- Update version constant in lib/shoulda/version.rb with new semver (
lib/shoulda/version.rb) - Add entry to CHANGELOG.md documenting all changes in this release (
CHANGELOG.md) - Follow release checklist in MAINTAINING.md to build and push gem (
MAINTAINING.md) - Gemspec automatically uses the updated version from lib/shoulda/version.rb (
shoulda.gemspec)
🔧Why these technologies
- Ruby/Minitest — Shoulda is designed to make Minitest and Test::Unit more expressive; Ruby is the natural host language.
- Rails integration — Shoulda matchers and context are specifically tailored for Rails model/controller testing; tight coupling is intentional.
- Umbrella gem pattern — Separates concerns between context DSL (shoulda-context) and assertion matchers (shoulda-matchers); allows independent updates and usage.
- GitHub Actions CI — Matrix testing across multiple Ruby and Rails versions ensures compatibility; appraisal system automates gemfile management.
⚖️Trade-offs already made
-
Umbrella gem with external dependencies instead of monolithic codebase
- Why: Allows shoulda-context and shoulda-matchers to be independently useful and versioned.
- Consequence: Users must trust and maintain two separate gems; coordination overhead for releases.
-
Test with acceptance tests rather than unit tests
- Why: Validates end-to-end integration with real Rails applications; catches real-world breakage.
- Consequence: Slower test suite; requires maintaining a test Rails app; harder to isolate failures.
-
Use Appraisals for multi-version testing
- Why: Tests across multiple Rails versions without polluting single Gemfile; industry standard for Rails gems.
- Consequence: Additional complexity in test setup; requires maintaining multiple gemfile.lock files.
🚫Non-goals (don't propose these)
- Does not provide its own assertion methods—delegates entirely to shoulda-context and shoulda-matchers
- Does not directly test code—only provides testing helpers and matchers for Rails projects
- Does not support testing frameworks other than Minitest/Test::Unit
- Does not handle non-Rails codebases (matchers are Rails-specific)
⚠️Anti-patterns to avoid
- Empty umbrella gem with zero logic —
lib/shoulda.rb: The gem contains no behavior of its own, only requires external gems. While intentional, this design
🪤Traps & gotchas
- This gem is just an umbrella—most test failures/feature requests belong in shoulda-context or shoulda-matchers repos, not here. 2. Appraisal setup required: scripts use
script/install_gems_in_all_appraisalsto set up test environment across Rails versions; runningbundle exec rake testalone without appraisal setup may not test all version combos. 3. Ruby version lock: .ruby-version and .tool-versions enforce specific Ruby version; check these before contributing. 4. Hound/Rubocop enforced: .hound.yml and .rubocop.yml enforce strict style; CI will fail linting before running tests.
🏗️Architecture
💡Concepts to learn
- Appraisal/Multi-version testing — Shoulda must work across multiple Rails versions (6.1, 7.0+) and Ruby versions (3.0+); appraisal gem solves this by creating separate Gemfile variants and running tests in each context, ensuring no version-specific breakage
- Umbrella gem pattern — Shoulda itself contains almost no code—it's a meta-gem that bundles two separate gems (shoulda-context, shoulda-matchers) for convenience; understanding this pattern explains why most issues belong downstream
- DSL (Domain-Specific Language) via metaprogramming — Shoulda's readability power comes from Ruby's metaprogramming (
context,shouldmethods defined in shoulda-context); understanding how these are monkey-patched into TestCase classes helps you extend or debug the DSL - Semantic Versioning (SemVer) — Shoulda follows SemVer 2.0 strictly (major.minor.patch), so breaking changes only happen on major version bumps; this is critical to understand when upgrading for compatibility guarantees
- Matcher pattern in testing — Shoulda Matchers implement a reusable matcher pattern (e.g.,
have_many,validate_presence_of) that encapsulates assertion logic and reads like English; this is the RSpec/Minitest standard for composable test assertions - CI/CD via GitHub Actions — Shoulda uses GitHub Actions workflows (ci.yml, rubocop.yml) to automate testing across Ruby/Rails versions, linting, and security scanning; understanding the workflow YAMLs is essential to modify test matrix or add checks
- Rails-specific testing (ActiveSupport::TestCase) — Shoulda's matchers are designed for Rails' ActiveSupport::TestCase (or Test::Unit), not generic Ruby—they understand Rails conventions (associations, validations, strong_parameters) and generate assertions accordingly
🔗Related repos
thoughtbot/shoulda-context— Provides thecontextandshouldblock DSL that Shoulda umbrella gem re-exports; this is where test organization/readability magic happensthoughtbot/shoulda-matchers— Provides the assertion matchers (e.g.,have_many,validate_presence_of,allow_value) that Shoulda umbrella gem bundles; where most of the testing DSL livesseattlerb/minitest— The underlying test runner that Shoulda (context + matchers) builds on; Shoulda works as an enhancement layer over Minitest assertionsruby/rake— Used by Shoulda's Rakefile to define test tasks and automations likerake testand appraisal-based test runsthoughtbot/factory_bot— Complementary gem often used alongside Shoulda for fixture/test data management in Rails test suites
🪄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 CI workflow for Ruby 3.2+ and Rails 8.0 compatibility
The repo has gemfiles for Rails 6.1 and 7.0, but the .github/workflows/ci.yml likely doesn't test against Rails 8.0 (released Dec 2023) or Ruby 3.2+. Given the project targets Rails developers and must maintain broad compatibility, testing against the latest stable versions is critical for early bug detection.
- [ ] Create gemfiles/rails_8_0.gemfile and gemfiles/rails_8_0.gemfile.lock with appropriate dependencies
- [ ] Update .github/workflows/ci.yml to include Rails 8.0 and Ruby 3.2+ in the test matrix
- [ ] Run script/run_all_tests locally to verify all appraisals pass
- [ ] Update MAINTAINING.md with instructions on adding new Rails versions
Add acceptance tests for newer Rails features integration
test/acceptance/integrates_with_rails_test.rb exists but likely only tests basic integration. Recent Rails versions introduced Zeitwerk autoloading (Rails 6+), async query support, and other features that could interact with shoulda's matchers. Adding specific acceptance tests ensures shoulda doesn't break with Rails updates.
- [ ] Expand test/acceptance/integrates_with_rails_test.rb to test Zeitwerk autoloading compatibility
- [ ] Add test case for shoulda matchers with async query patterns (if applicable to shoulda's scope)
- [ ] Add test case for Rails 8.0-specific features (e.g., query annotation)
- [ ] Verify tests pass with script/run_all_tests
Document and test the umbrella gem structure with gem dependency matrix
The README states 'the shoulda gem doesn't contain any code of its own but rather brings in behavior' but cuts off mid-sentence. The repo should clearly document which sub-gems (shoulda-matchers, shoulda-context, etc.) are included, their versions, and compatibility matrix with different Rails/Ruby versions. Add tests to verify all sub-gem dependencies load correctly.
- [ ] Complete the truncated sentence in README.md explaining the umbrella structure and list all bundled gems
- [ ] Add test/unit/gem_dependencies_test.rb to verify all required sub-gems are properly required and loadable
- [ ] Create a dependency compatibility matrix table in MAINTAINING.md showing shoulda version → sub-gem versions → Rails versions
- [ ] Update shoulda.gemspec to document why certain sub-gems are dependencies and their minimum versions
🌿Good first issues
- Update CHANGELOG.md with a summary of the latest Shoulda Context/Shoulda Matchers releases and link to their CHANGELOGs—currently it's minimal and could help users understand what features come with each version.
- Expand MAINTAINING.md with a 'Upgrading Dependencies' section documenting how to bump shoulda-context and shoulda-matchers versions and run the full test matrix—useful for future maintainers.
- Add a test in test/acceptance/ that verifies Shoulda works correctly with the latest Rails 7.0 features (e.g., Zeitwerk autoloading, Fiber-based concurrency) by creating a minimal Rails app and running shoulda tests against it.
⭐Top contributors
Click to expand
Top contributors
- @mcmire — 22 commits
- @rmm5t — 22 commits
- @tmcgilchrist — 7 commits
- @matsales28 — 5 commits
- @github-actions[bot] — 5 commits
📝Recent commits
Click to expand
Recent commits
9d2b2e2— Merge pull request #287 from thoughtbot/github-actions/repository-maintenance-2cf2e459fcabdd661e5175d85887b996cc70c520 (stefannibrasil)87f0f9a— Merge pull request #288 from thoughtbot/github-actions/repository-maintenance-bd381d30aa4069aa5ff29409fbea5df7f1aa92b1 (matsales28)29c5bcf— docs: documentation files updated (github-actions[bot])bd381d3— Merge pull request #283 from thoughtbot/add-codeowners (matsales28)b2ac7b4— docs: documentation files updated (github-actions[bot])2cf2e45— Merge pull request #286 from thoughtbot/github-actions/repository-maintenance-6446255a6e0c43aca854265de5f8812f64cc28f9 (stefannibrasil)8aa1df5— chore: Add CODEOWNERS file (matsales28)ce5163a— docs: documentation files updated (github-actions[bot])6446255— Merge pull request #281 from thoughtbot/sb-dynamic-readme-workflow (matsales28)fc10603— Merge pull request #285 from thoughtbot/github-actions/repository-maintenance-2ee7cc3d71f7f22d00ee9162be49cd1fb87575da (matsales28)
🔒Security observations
The shoulda gem repository demonstrates a reasonable security posture with established CI/CD workflows and a security policy in place. However, there are opportunities for improvement in vulnerability management practices. The main concerns are: (1) restrictive single-version support policy that may leave users vulnerable, (2) lack of explicit security contacts and SLAs, (3) no visible automated dependency vulnerability scanning, and (4) minimal security documentation. No critical vulnerabilities were identified in the visible codebase structure. The gem is a testing utility with minimal attack surface (test helpers and matchers). Recommendations focus on strengthening the security policy, adding automated scanning, and improving communication channels for security issues.
- Medium · Incomplete Security Policy - Single Version Support —
SECURITY.md. The SECURITY.md policy states that only the latest version is supported. This is a very restrictive policy that may leave users vulnerable if they cannot immediately update. No information is provided about security patch backports or timelines for critical issues in older versions. Fix: Consider implementing a more nuanced support policy (e.g., support last 2-3 versions or provide a security patch period). Document clear timelines for security vulnerability patches and consider backporting critical security fixes to recent previous versions. - Low · Missing Security Contact Information —
SECURITY.md. While a security reporting URL is provided, there is no direct security contact email or PGP key for responsible disclosure. The policy redirects to a generic security page without clear escalation paths or expected response times. Fix: Add explicit security contact email address, expected response time SLA, and consider publishing a PGP key for encrypted vulnerability reports. Include clear vulnerability disclosure timeline expectations. - Low · No Dependency Vulnerability Scanning Configuration —
.github/workflows/. The codebase includes CI/CD workflows but no evidence of automated dependency vulnerability scanning (e.g., Dependabot, Snyk, or bundler-audit) in the provided configuration files. Fix: Enable automated dependency vulnerability scanning in CI/CD pipeline. Consider enabling Dependabot or similar tools to automatically detect and alert on vulnerable gem dependencies. - Low · Minimal Security Documentation —
SECURITY.md. The SECURITY.md file is minimal and provides limited guidance on security best practices, responsible disclosure procedures, or security considerations for users of the gem. Fix: Expand security documentation to include: security best practices for users, known security considerations, responsible disclosure process details, and security release notes history.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.