一键导入
testing-standards
kw-app testing standards using RSpec, FactoryBot, and Docker. Covers test types, patterns, and best practices for TDD workflow.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
kw-app testing standards using RSpec, FactoryBot, and Docker. Covers test types, patterns, and best practices for TDD workflow.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
ActiveRecord model patterns and best practices for kw-app - validations, associations, scopes, queries, and when to keep models thin.
kw-app mandatory pattern for service objects using dry-monads Result monad with Success/Failure and do-notation. Replaces deprecated custom Result classes.
Zero-downtime deployment with Kamal for kw-app (staging on Raspberry Pi ARM64, production on VPS x86_64). Covers deployment workflows, console access, and troubleshooting.
Performance optimization patterns for kw-app - N+1 query prevention, eager loading, caching strategies, and database optimization.
Service object architecture for kw-app using dry-monads Result monad. Covers when to use services, structure patterns, testing, and integration with controllers.
| name | testing-standards |
| description | kw-app testing standards using RSpec, FactoryBot, and Docker. Covers test types, patterns, and best practices for TDD workflow. |
| allowed-tools | Read, Write, Edit, Bash |
Test Framework: RSpec 3.x
Fixtures: FactoryBot
Environment: Docker (MANDATORY)
Philosophy: TDD (RED → GREEN → REFACTOR)
# ✅ Correct - Run tests in Docker
docker-compose exec -T app bundle exec rspec
# ❌ Wrong - Don't run on host
bundle exec rspec # Wrong Ruby version, no PostgreSQL/Redis
Why Docker is mandatory:
# All tests
docker-compose exec -T app bundle exec rspec
# Specific file
docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb
# Specific line
docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb:25
# Pattern match
docker-compose exec -T app bundle exec rspec spec/models/
# Verbose output (debugging)
docker-compose exec -T app env VERBOSE_TESTS=true bundle exec rspec
# Fast fail (stop on first failure)
docker-compose exec -T app bundle exec rspec --fail-fast
# Documentation format
docker-compose exec -T app bundle exec rspec --format documentation
# Rails console (for debugging tests)
docker-compose exec app bundle exec rails console
spec/
├── models/ # Model specs (validations, associations, scopes)
├── components/ # Service object / operation specs
│ └── users/
│ └── operation/
│ └── create_spec.rb
├── requests/ # HTTP integration tests (PREFERRED over controller specs)
├── factories/ # FactoryBot factories
├── support/ # Shared contexts, helpers
│ ├── factory_bot.rb
│ ├── database_cleaner.rb
│ └── shared_examples/
└── rails_helper.rb # Main test configuration
| What to Test | Test Type | Example |
|---|---|---|
| Model validations | Model spec | spec/models/user_spec.rb |
| Service objects | Component spec | spec/components/users/operation/create_spec.rb |
| HTTP endpoints | Request spec | spec/requests/users_spec.rb |
| Background jobs | Job spec | spec/jobs/user_notification_job_spec.rb |
| Mailers | Mailer spec | spec/mailers/user_mailer_spec.rb |
Don't write:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'associations' do
it { should have_many(:posts) }
it { should belong_to(:company) }
end
describe 'validations' do
subject { build(:user) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
it { should validate_length_of(:name).is_at_least(2).is_at_most(100) }
end
describe 'scopes' do
describe '.active' do
let!(:active_user) { create(:user, active: true) }
let!(:inactive_user) { create(:user, active: false) }
it 'returns only active users' do
expect(User.active).to contain_exactly(active_user)
end
end
end
describe '#full_name' do
let(:user) { build(:user, first_name: 'John', last_name: 'Doe') }
it 'combines first and last name' do
expect(user.full_name).to eq('John Doe')
end
end
end
build(:user) for validation tests (no DB hit)create(:user) when testing associations/scopessubject for repeated objectsdescribe or context# spec/components/users/operation/create_spec.rb
require 'rails_helper'
RSpec.describe Users::Operation::Create do
subject(:operation) { described_class.new }
describe '#call' do
let(:params) { { email: 'test@example.com', name: 'Test User' } }
context 'with valid params' do
it 'returns Success with user' do
result = operation.call(params: params)
expect(result).to be_success
expect(result.success).to be_a(User)
expect(result.success.email).to eq('test@example.com')
end
it 'creates a user record' do
expect { operation.call(params: params) }
.to change(User, :count).by(1)
end
it 'sends welcome email' do
expect(UserMailer).to receive(:welcome).and_call_original
operation.call(params: params)
end
end
context 'with invalid params' do
let(:params) { { email: '', name: '' } }
it 'returns Failure with errors' do
result = operation.call(params: params)
expect(result).to be_failure
expect(result.failure).to be_a(Hash)
expect(result.failure).to include(:email, :name)
end
it 'does not create a user' do
expect { operation.call(params: params) }
.not_to change(User, :count)
end
end
context 'with duplicate email' do
before { create(:user, email: 'test@example.com') }
let(:params) { { email: 'test@example.com', name: 'Other User' } }
it 'returns Failure' do
result = operation.call(params: params)
expect(result).to be_failure
end
end
end
end
be_success and be_failure matchers# spec/requests/users_spec.rb
require 'rails_helper'
RSpec.describe 'Users', type: :request do
describe 'POST /users' do
let(:valid_params) do
{ user: { email: 'test@example.com', name: 'Test User' } }
end
context 'with valid params' do
it 'creates a new user' do
expect { post users_path, params: valid_params }
.to change(User, :count).by(1)
end
it 'returns 201 created' do
post users_path, params: valid_params
expect(response).to have_http_status(:created)
end
it 'returns user data' do
post users_path, params: valid_params
json = JSON.parse(response.body)
expect(json['email']).to eq('test@example.com')
end
end
context 'with invalid params' do
let(:invalid_params) { { user: { email: '', name: '' } } }
it 'does not create a user' do
expect { post users_path, params: invalid_params }
.not_to change(User, :count)
end
it 'returns 422 unprocessable entity' do
post users_path, params: invalid_params
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns error messages' do
post users_path, params: invalid_params
json = JSON.parse(response.body)
expect(json['errors']).to be_present
end
end
context 'when not authenticated' do
it 'returns 401 unauthorized' do
post users_path, params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET /users/:id' do
let(:user) { create(:user) }
it 'returns the user' do
get user_path(user)
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['id']).to eq(user.id)
end
context 'when user does not exist' do
it 'returns 404 not found' do
get user_path(id: 99999)
expect(response).to have_http_status(:not_found)
end
end
end
end
have_http_status matcher# spec/factories/users.rb
FactoryBot.define do
factory :user do
email { Faker::Internet.email }
name { Faker::Name.name }
active { true }
trait :inactive do
active { false }
end
trait :admin do
role { 'admin' }
end
trait :with_posts do
transient do
posts_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end
end
# Basic
user = build(:user) # Not saved
user = create(:user) # Saved to DB
# With traits
admin = create(:user, :admin)
inactive = create(:user, :inactive)
# Override attributes
user = create(:user, email: 'specific@example.com')
# With associations
user = create(:user, :with_posts)
# Lists
users = create_list(:user, 5)
admins = create_list(:user, 3, :admin)
Faker for random datatraits for variationstransient for countsafter(:create) for associations# Stub external API call
allow(PaymentGateway).to receive(:charge).and_return(true)
# Mock with expectations
expect(UserMailer).to receive(:welcome)
.with(user)
.and_call_original
# Stub with specific return
allow(InventoryService).to receive(:available?)
.with(product_id, quantity)
.and_return(true)
# Stub to raise error
allow(PaymentGateway).to receive(:charge)
.and_raise(PaymentGateway::Error, 'Card declined')
# Freeze time
freeze_time = Time.zone.parse('2024-01-15 10:00:00')
travel_to(freeze_time) do
# Tests here see fixed time
end
# Travel forward
travel 1.day do
# Tests see tomorrow
end
# spec/support/shared_examples/trackable.rb
RSpec.shared_examples 'trackable' do
it { should have_db_column(:created_at).of_type(:datetime) }
it { should have_db_column(:updated_at).of_type(:datetime) }
describe 'timestamps' do
let(:record) { create(described_class.name.underscore) }
it 'sets created_at on create' do
expect(record.created_at).to be_present
end
it 'updates updated_at on save' do
original = record.updated_at
travel 1.minute do
record.touch
expect(record.updated_at).to be > original
end
end
end
end
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
it_behaves_like 'trackable'
end
1. RED: Write failing test
docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb
❌ Test fails (expected)
2. GREEN: Write minimum code to pass
docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb
✅ Test passes
3. REFACTOR: Improve code
docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb
✅ Test still passes
4. Repeat
# 1. RED - Write failing test
describe 'validations' do
it { should validate_presence_of(:email) }
end
# Run: ❌ Fails
# 2. GREEN - Add validation
class User < ApplicationRecord
validates :email, presence: true
end
# Run: ✅ Passes
# 3. REFACTOR - Add format validation
describe 'validations' do
it { should validate_presence_of(:email) }
it { should allow_value('test@example.com').for(:email) }
it { should_not allow_value('invalid').for(:email) }
end
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end
# Run: ✅ Still passes
# With SimpleCov
COVERAGE=true docker-compose exec -T app bundle exec rspec
# View report
open coverage/index.html
# ❌ Wrong - uses system Ruby
bundle exec rspec
Fix:
# ✅ Correct - uses Docker Ruby
docker-compose exec -T app bundle exec rspec
# ❌ Wrong - hardcoded data
user = User.create!(email: 'test@test.com', name: 'Test')
Fix:
# ✅ Correct - uses factory
user = create(:user)
# ❌ Wrong - multiple assertions
it 'creates user and sends email and updates counter' do
expect { operation.call }.to change(User, :count).by(1)
expect(UserMailer).to have_received(:welcome)
expect(Rails.cache.read('user_count')).to eq(1)
end
Fix:
# ✅ Correct - separate examples
it 'creates user' do
expect { operation.call }.to change(User, :count).by(1)
end
it 'sends welcome email' do
expect(UserMailer).to receive(:welcome)
operation.call
end
it 'updates counter' do
operation.call
expect(Rails.cache.read('user_count')).to eq(1)
end
# ❌ Wrong - data persists between tests
before(:all) do
@user = create(:user)
end
Fix:
# ✅ Correct - clean data per test
before(:each) do
@user = create(:user)
end
| Task | Command |
|---|---|
| Run all tests | docker-compose exec -T app bundle exec rspec |
| Run specific file | docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb |
| Run specific line | docker-compose exec -T app bundle exec rspec spec/models/user_spec.rb:25 |
| Verbose output | docker-compose exec -T app env VERBOSE_TESTS=true bundle exec rspec |
| Fast fail | docker-compose exec -T app bundle exec rspec --fail-fast |
| With coverage | COVERAGE=true docker-compose exec -T app bundle exec rspec |
Version: 2.0
Last Updated: 2024-01
Maintained By: kw-app team