kw-app testing standards using RSpec, FactoryBot, and Docker. Covers test types, patterns, and best practices for TDD workflow.
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
npx skills add rockcodelabs/testing-standards下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer