rspec-service-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

RSpec Service Testing

RSpec服务测试

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.
当为
spec/services/
下的服务类编写测试时使用本技能。
核心原则: 测试公共契约(
.call
.find
.search
),而非内部实现。使用
instance_double
实现隔离,使用
create
进行集成测试。

Workflow: Write → Run → Verify → Fix

工作流程:编写 → 运行 → 验证 → 修复

text
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.
text
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
切勿在步骤1完成且因正确原因导致失败前实现服务。

Quick Reference

快速参考

AspectRule
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 responsesFactoryBot hash factories (
class: Hash
)
方面规则
文件位置
spec/services/module_name/service_spec.rb
测试主体
subject(:service_call) { described_class.call(params) }
单元隔离为协作者使用
instance_double
集成测试针对数据库相关测试使用
create
多断言使用
aggregate_failures
状态验证使用
change
匹配器
时间相关逻辑使用
travel_to
API响应使用FactoryBot哈希工厂(
class: Hash

Spec Template

测试用例模板

ruby
undefined
ruby
undefined

frozen_string_literal: true

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:

```ruby
let(:client) { instance_double(Api::Client) }
before { allow(client).to receive(:execute_query).and_return(api_response) }
Use
create
for integration tests:
ruby
let(:source_shelter) { create(:shelter, :with_animals) }
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

使用`instance_double`实现单元隔离:

```ruby
let(:client) { instance_double(Api::Client) }
before { allow(client).to receive(:execute_query).and_return(api_response) }
使用
create
进行集成测试:
ruby
let(:source_shelter) { create(:shelter, :with_animals) }

FactoryBot Hash Factories for API Responses

用于API响应的FactoryBot哈希工厂

When testing API clients, use
class: Hash
with
initialize_with
to build hash-shaped response fixtures — see PATTERNS.md for the full pattern and factory placement.
测试API客户端时,结合
class: Hash
initialize_with
构建哈希格式的响应测试数据——完整模式和工厂放置位置请参阅PATTERNS.md

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
  • Logger expectations for error logging
  • 为主要动作定义
    subject
  • 单元测试使用
    instance_double
    / 集成测试使用
    create
  • 为每个公共方法编写正常路径测试
  • 错误和边缘场景测试(空输入、无效引用、失败情况)
  • 相关的部分成功场景测试
  • 为重复模式使用
    shared_examples
  • 为多断言测试使用
    aggregate_failures
  • 为状态验证使用
    change
    匹配器
  • 针对错误日志添加日志器断言

Common Mistakes

常见错误

MistakeCorrect approach
No error scenario testsHappy path only = false confidence — always test failures
let!
everywhere
Use
let
(lazy) unless the value is needed unconditionally for setup
Huge factory setupKeep factories minimal — only attributes required for the test
Spec breaks when implementation changes but behavior is unchangedTests that break on refactoring are testing internals, not contracts
错误做法正确做法
未测试错误场景仅测试正常路径会导致虚假的信心——务必测试失败场景
到处使用
let!
除非设置需要无条件使用该值,否则使用
let
(延迟加载)
庞大的工厂设置保持工厂精简——仅保留测试所需的属性
当实现变更但行为未变时测试用例失效重构时失效的测试是在测试内部实现,而非契约

Integration

集成

SkillWhen to chain
rspec-best-practicesFor general RSpec style and TDD discipline
ruby-service-objectsFor the service conventions being tested
ruby-api-client-integrationFor API client layer testing patterns
rails-engine-testingWhen testing engine-specific services
技能何时关联使用
rspec-best-practices用于通用RSpec风格和TDD规范
ruby-service-objects用于被测试的服务约定
ruby-api-client-integration用于API客户端层测试模式
rails-engine-testing测试引擎专属服务时使用