agentleFS
igmarinigmarin/rails-agent-skills/skills/test-service/SKILL.md

test-service

Skill26 starsChanged 4 months agoMITView on GitHub

Use when writing RSpec for a service object under spec/services/. Test the public .call contract. Trigger words: service spec, test service object, spec/services.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use instance_double for unit isolation:

    line 88
  2. Very specific:

    Avoid class-level stubs: do not use allow(CarrierApi::Client).to receive(:notify) — always double the instance.

    line 98
  3. Very specific:

    Use create for integration tests:

    line 100
  4. Very specific:

    When testing API clients, use class: Hash with initialize_with to build hash-shaped response fixtures.

    line 108
  5. Distinctive:

    When completing a service test, output MUST include:

    line 154

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Test Service

Quick Reference

AspectRule
File locationspec/services/module_name/service_spec.rb
Subjectsubject(:service_call) { described_class.call(params) }
Unit isolationinstance_double for collaborators
Integrationcreate for DB-backed tests
Multi-assertionaggregate_failures
State verificationchange matchers
Time-dependenttravel_to
API responsesFactoryBot hash factories (class: Hash)

HARD-GATE

DO NOT implement the service before step 1 is written and failing for the right reason.
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

Core Process

Spec Template

# 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) }

CRITICAL — Collaborators MUST be stubbed via instance_double. Three patterns:

  • Inject dependency: pass the double in params directly.
  • Stub .new: allow(CarrierApi::Client).to receive(:new).and_return(client) when the service instantiates internally.
  • Avoid class-level stubs: do not use allow(CarrierApi::Client).to receive(:notify) — always double the instance.

Use create for integration tests:

let(:source_shelter) { create(:shelter, :with_animals) }

FactoryBot Hash Factories for API Responses

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') }

New Test File Checklist

  • subject defined for the main action
  • instance_double for unit / create for integration
  • Happy path for each public method
  • Error and edge cases (blank input, invalid refs, failures)
  • Partial success scenarios where relevant
  • shared_examples for repeated patterns
  • aggregate_failures for multi-assertion tests
  • change matchers for state verification

Common Mistakes

MistakeCorrect approach
No error scenario testsAlways test failures alongside the happy path
let! everywhereUse let (lazy) unless the value is unconditionally required for setup
Huge factory setupKeep factories minimal — only attributes the test requires
Spec breaks on refactor with unchanged behaviorTests that break on refactoring are testing internals, not contracts

Extended Resources (Progressive Disclosure)

Load these files only when their specific content is needed:

  • PATTERNS.md — Use when you need the full pattern catalog and factory placement guidance
  • assets/spec_examples.md — Use when you need additional worked examples beyond the spec template above
  • assets/testing_checklist.md — Use when reviewing a completed service spec for completeness

Output Style

When completing a service test, output MUST include:

# Service Spec — [ServiceName]

## Spec File
- Path: spec/services/<module>/<service>_spec.rb
- Subject: `described_class.call(params)`

## Coverage
- Happy path: ✓ (<n> examples)
- Error cases: ✓ (<n> examples — list error classes/conditions)
- Edge cases: ✓ (<n> examples — blank input, boundary values)
- Isolation: instance_double for <collaborator list>

## TDD Gate
- RED: <failure message confirming missing behavior>
- GREEN: <all examples pass>
- Suite: <full spec/services/ suite status>

Integration

SkillWhen to chain
write-testsFor general RSpec style and TDD discipline
create-service-objectFor the service conventions being tested
integrate-api-clientFor API client layer testing patterns
test-engineWhen testing engine-specific services

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.