From rails-agent-skills
Guides RSpec testing for service objects, API clients, orchestrators, and business logic in spec/services/, using instance_double, FactoryBot, aggregate_failures, change matchers, and travel_to.
npx claudepluginhub igmarin/rails-agent-skills --plugin rails-agent-skillsThis skill uses the workspace's default tool permissions.
Use this skill when writing tests for service classes under `spec/services/`.
Writes, fixes, and refactors RSpec tests for Ruby/Rails apps using describe/it blocks, expect matchers, test doubles, FactoryBot, and TDD best practices. Activates on spec files or test queries.
Guides writing, reviewing, and refactoring RSpec tests for Ruby and Rails apps. Covers spec types (model/request/system/job), factories, TDD workflow, flaky fixes, and deterministic assertions.
Assists writing, reviewing, and improving RSpec tests for Ruby on Rails apps including model, controller, system, and integration specs using Better Specs and thoughtbot best practices.
Share bugs, ideas, or general feedback.
Use this skill when writing tests for service classes under spec/services/.
Core principle: Test the public contract (.call, .find, .search), not internal implementation. Use instance_double for isolation, create for integration.
1. WRITE: Write the spec (happy path + error cases + edge cases)
2. RUN: bundle exec rspec spec/services/your_service_spec.rb
3. VERIFY: Confirm failures are for the right reason (not a typo or missing factory)
4. FIX: Implement or fix until the spec passes
5. SUITE: bundle exec rspec spec/services/ — verify no regressions
DO NOT implement the service before step 1 is written and failing for the right reason.
| Aspect | Rule |
|---|---|
| File location | spec/services/module_name/service_spec.rb |
| Subject | subject(:service_call) { described_class.call(params) } |
| Unit isolation | instance_double for collaborators |
| Integration | create for DB-backed tests |
| Multi-assertion | aggregate_failures |
| State verification | change matchers |
| Time-dependent | travel_to |
| API responses | FactoryBot hash factories (class: Hash) |
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ModuleName::MainService do
describe '.call' do
subject(:service_call) { described_class.call(params) }
let(:shelter) { create(:shelter, :with_animals) }
let(:params) do
{ shelter: { shelter_id: shelter.id }, items: %w[TAG001 TAG002] }
end
context 'when input is valid' do
before { create(:animal, tag_number: 'TAG001', shelter:) }
it 'returns success' do
expect(service_call[:success]).to be true
end
end
context 'when shelter is not found' do
let(:params) { super().merge(shelter: { shelter_id: 999_999 }) }
it 'returns error response' do
expect(service_call[:success]).to be false
end
end
context 'when input is blank' do
let(:params) { { shelter: { shelter_id: nil }, items: [] } }
it 'returns error response with meaningful message' do
aggregate_failures do
expect(service_call[:success]).to be false
expect(service_call[:errors]).not_to be_empty
end
end
end
end
end
Use instance_double for unit isolation:
let(:client) { instance_double(Api::Client) }
before { allow(client).to receive(:execute_query).and_return(api_response) }
Use create for integration tests:
let(:source_shelter) { create(:shelter, :with_animals) }
When testing API clients, use class: Hash with initialize_with to build hash-shaped response fixtures. A minimal example:
FactoryBot.define do
factory :api_animal_response, class: Hash do
tag_number { 'TAG001' }
status { 'active' }
initialize_with { attributes.stringify_keys }
end
end
# In the spec:
let(:api_response) { build(:api_animal_response, tag_number: 'TAG002') }
See PATTERNS.md for the full pattern and factory placement guidance.
subject defined for the main actioninstance_double for unit / create for integrationshared_examples for repeated patternsaggregate_failures for multi-assertion testschange matchers for state verification| Mistake | Correct approach |
|---|---|
| No error scenario tests | Happy path only = false confidence — always test failures |
let! everywhere | Use let (lazy) unless the value is needed unconditionally for setup |
| Huge factory setup | Keep factories minimal — only attributes required for the test |
| Spec breaks when implementation changes but behavior is unchanged | Tests that break on refactoring are testing internals, not contracts |
| Skill | When to chain |
|---|---|
| rspec-best-practices | For general RSpec style and TDD discipline |
| ruby-service-objects | For the service conventions being tested |
| ruby-api-client-integration | For API client layer testing patterns |
| rails-engine-testing | When testing engine-specific services |