testing-patterns
npx skills add https://github.com/rolemodel/rolemodel-skills --skill testing-patterns
Agent 安装分布
Skill 文档
Testing Patterns
Overview
Write automated tests using RSpec and Capybara. Avoid using the Rails console or starting a Rails server for testing.
Test Command
bundle exec rspec
Tech Stack
- RSpec – Testing framework
- Capybara – System/integration testing
- FactoryBot – Test data generation
Best Practices
General Guidelines
- Write tests first or alongside implementation
- Avoid manual testing via console
- Use factories for test data creation
- Keep tests focused and readable
RSpec Conventions
- Use descriptive context and describe blocks
- Follow the arrange-act-assert pattern
- Use let for test data setup
- Prefer
letover instance variables
let vs let! (Lazy vs Eager Evaluation)
Use let (lazy evaluation) when:
- The variable is explicitly referenced in the test
- You want to avoid unnecessary database writes
- The record creation has side effects you want to control
Use let! (eager evaluation) when:
- Records must exist in the database before the test runs
- The variable is not directly referenced but its existence is required
- Testing queries that search for records (e.g., index pages, search functionality)
- Setting up background data that other records depend on
Example – System Tests:
RSpec.describe 'Job Management', type: :system do
# Use let! - these records must exist in DB for dropdowns and queries
let!(:location) { create(:location, name: 'Downtown Site') }
let!(:superintendent) { create(:user, :superintendent) }
# Use let - only created when explicitly referenced in a test
let(:job) { create(:job, name: 'Test Job', location:, superintendent:) }
it 'shows location in dropdown' do
visit new_job_path
# location must exist in DB for dropdown to display it
expect(page).to have_select('Location', with_options: [location.name])
end
it 'can delete a job' do
visit job_path(job) # job created here when first referenced
click_button 'Delete'
end
end
Common Pitfall:
# â WRONG - Test will fail because job2 doesn't exist in DB yet
let(:job1) { create(:job, name: 'Job 1') }
let(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # job1 created when referenced
expect(page).to have_content('Job 2') # FAIL - job2 never referenced, not in DB
end
# â
CORRECT - Both jobs exist before test runs
let!(:job1) { create(:job, name: 'Job 1') }
let!(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # Both jobs already in DB
expect(page).to have_content('Job 2') # Test passes
end
Key Insight:
If you expect to see data without explicitly interacting with the object variable (like viewing a list or selecting from a dropdown), use let! to ensure the record exists in the database.
Validation Testing Pattern
Test validations explicitly using build with invalid data, then verify the model is invalid and check error messages:
describe 'validations' do
it 'must have a start date' do
membership = build(:membership, start_date: nil)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly "Start date can't be blank"
end
it 'enforces end date must follow start date' do
membership = build(:membership, start_date: 1.year.ago, end_date: 2.years.ago)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly 'End date must follow start date'
end
it 'permits empty end date' do
membership = build(:membership, start_date: 1.year.ago, end_date: nil)
expect(membership).to be_valid
end
end
Key points:
- Use
buildinstead ofcreateto avoid database writes - Test both invalid and valid scenarios
- Verify exact error messages with
full_messages - Use descriptive test names that explain the business rule
- Don’t test associations or enums
Capybara System Tests
- Test user-facing functionality
- Use
data-testidattributes withdom_idfor reliable element selection - Test happy paths and edge cases
- Ensure tests are deterministic
- Avoid sleep statements; use Capybara’s waiting mechanisms such as native expectations of elements to appear
- Use
:js(e.g.it 'does something', :js do) for specs that run javascript such as stimulus controllers
Element Selection with data-testid
Use data-testid attributes with dom_id for stable, reliable element selection that’s resistant to UI changes:
View:
tbody
- @entries.each do |entry|
tr data-testid=dom_id(entry)
td= entry.name
Spec:
within(data_test(entry1)) do
click_button 'Submit'
end
Benefits:
- Resilient to text changes (descriptions, labels, etc.)
- Works with dynamic content
- Self-documenting test intent
- Easier to refactor views
Avoid:
- Text-based lookups:
within('tr', text: 'Entry 1') - CSS class selectors that may change during styling
- Overly specific DOM traversal
Scoping with within Blocks
When elements are ambiguous (multiple buttons/links with same text), use within blocks to scope interactions:
Best Practice:
Always use within blocks when:
- Multiple elements share the same text/label
- Interacting with modals, panels, or overlays
- Working with repeating elements (table rows, cards)
- Tests fail with “Ambiguous match” errors
Turbo Confirm Dialogs
When testing actions that trigger Turbo confirm dialogs (e.g., delete buttons with data: { turbo_confirm: 'message' }), use the provided helper methods.
Setup:
Create the helper file:
# spec/support/turbo_confirm_helper.rb
module TurboConfirmHelper
def accept_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-accept').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
def deny_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-cancel').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
end
Include in RSpec configuration:
# spec/support/helpers.rb
RSpec.configure do |c|
# ...existing code...
c.include TurboConfirmHelper, type: :system
end
Usage in Tests:
accept_turbo_confirm do
click_button 'Delete'
end
deny_turbo_confirm do
click_button 'Delete'
end
Key Points:
- Always use
:jstag for tests involving Turbo confirm dialogs - Pass the action that triggers the confirm as a block to the helper
- The helper automatically waits for the dialog to appear and disappear
- Use
accept_turbo_confirmto click “Yes, I’m Sure” - Use
deny_turbo_confirmto click “Cancel” - Helpers include proper wait times and scoping for reliability
FactoryBot
- Define factories for all models
- Use traits for variations
- Keep factories minimal
- Override attributes in tests as needed
- Always use
buildorcreateinstead of direct model instantiation - Use
buildfor validation tests to avoid database writes - Use
createwhen you need persisted records
Future Topics
- Mocking and stubbing patterns
- Test organization strategies
- Performance testing
- CI/CD integration