spec-performance
This skill should be used when the user asks about "slow specs", "test performance", "parallel tests", "spec profiling", "let vs let!", "build vs create", "test optimization", or needs guidance on making RSpec tests faster.
When & Why to Use This Skill
This Claude skill optimizes RSpec test suite performance by providing expert guidance on profiling slow specs, database interaction strategies, and parallel testing. It helps developers reduce CI wait times and improve local development productivity through proven Ruby testing best practices like stubbing, lazy evaluation, and efficient factory usage.
Use Cases
- Identifying and ranking the slowest examples in a large Ruby on Rails test suite using built-in RSpec profiling tools.
- Refactoring database-heavy tests by switching from 'create' to 'build' or 'build_stubbed' to minimize expensive disk I/O.
- Configuring parallel testing environments to utilize multi-core processors and significantly decrease total test execution time.
- Optimizing test data setup by replacing redundant 'let!' calls with lazy-evaluated 'let' or implementing batch database inserts.
- Speeding up specs by stubbing external API calls and time-consuming methods using WebMock and RSpec mocks.
| name | Spec Performance |
|---|---|
| description | This skill should be used when the user asks about "slow specs", "test performance", "parallel tests", "spec profiling", "let vs let!", "build vs create", "test optimization", or needs guidance on making RSpec tests faster. |
| version | 1.0.0 |
Spec Performance
Slow tests reduce productivity and discourage running tests frequently. This skill covers techniques for optimizing RSpec test suite performance.
Profiling Slow Tests
Built-in Profiler
# Show 10 slowest examples
rspec --profile 10
# In spec_helper.rb for always-on profiling
RSpec.configure do |config|
config.profile_examples = 10
end
Detailed Timing
# spec/support/timing.rb
RSpec.configure do |config|
config.around(:each) do |example|
start = Time.now
example.run
elapsed = Time.now - start
puts "#{example.full_description}: #{elapsed.round(2)}s" if elapsed > 1
end
end
Database Optimization
Prefer build Over create
# Slow - hits database
let(:user) { create(:user) }
# Fast - in memory only
let(:user) { build(:user) }
# Fastest - stubbed, no DB
let(:user) { build_stubbed(:user) }
When to Use Each Strategy
| Strategy | Use When |
|---|---|
build |
Testing validations, object behavior |
build_stubbed |
Need ID, testing presentation |
create |
Testing DB queries, associations, callbacks |
Minimize Database Writes
# Slow - creates 3 users
it "lists users" do
create(:user)
create(:user)
create(:user)
expect(User.count).to eq(3)
end
# Better - single batch insert
it "lists users" do
User.insert_all([
{ email: "a@test.com" },
{ email: "b@test.com" },
{ email: "c@test.com" }
])
expect(User.count).to eq(3)
end
Use before(:all) Carefully
# Creates user once for all examples
before(:all) do
@user = create(:user)
end
after(:all) do
@user.destroy
end
# Warning: shared state between examples
# Use only for read-only data
let vs let!
let - Lazy Evaluation
let(:user) { create(:user) }
it "does something" do
# User created here, when first accessed
user.name
end
it "does something else" do
# User not created if not referenced
expect(true).to be true
end
let! - Eager Evaluation
let!(:user) { create(:user) }
it "has a user in database" do
# User already created before this runs
expect(User.count).to eq(1)
end
When to Use let!
# Use let! when:
# 1. Callback side effects needed
let!(:user) { create(:user) } # Triggers after_create callbacks
# 2. Database state must exist before test
let!(:existing_user) { create(:user, email: "taken@test.com") }
it "validates uniqueness" do
new_user = build(:user, email: "taken@test.com")
expect(new_user).not_to be_valid
end
Avoid Unnecessary let!
# Bad - always creates even when not needed
let!(:user) { create(:user) }
let!(:post) { create(:post, user: user) }
let!(:comment) { create(:comment, post: post) }
# Good - only create what's needed per test
let(:user) { create(:user) }
context "with posts" do
let(:post) { create(:post, user: user) }
it "lists posts" do
post # Triggers creation
expect(user.posts).to include(post)
end
end
Parallel Testing
parallel_tests Gem
# Gemfile
gem "parallel_tests", group: [:development, :test]
# Setup
rake parallel:setup
rake parallel:create
rake parallel:migrate
# Run tests
rake parallel:spec
# or
parallel_rspec spec/
Database Configuration
# config/database.yml
test:
database: myapp_test<%= ENV['TEST_ENV_NUMBER'] %>
Avoiding Parallelization Issues
# Use unique data per process
let(:email) { "user#{Process.pid}@test.com" }
# Avoid shared files
let(:file_path) { Rails.root.join("tmp/test_#{Process.pid}.txt") }
Mocking and Stubbing for Speed
Stub External Services
# Slow - real HTTP call
it "fetches data" do
result = ExternalApi.fetch(id: 1)
expect(result).to be_present
end
# Fast - stubbed response
it "fetches data" do
allow(ExternalApi).to receive(:fetch).and_return({ data: "test" })
result = ExternalApi.fetch(id: 1)
expect(result).to eq({ data: "test" })
end
Use WebMock for HTTP
# Gemfile
gem "webmock", group: :test
# spec/spec_helper.rb
require "webmock/rspec"
WebMock.disable_net_connect!(allow_localhost: true)
# In specs
stub_request(:get, "https://api.example.com/users")
.to_return(status: 200, body: { users: [] }.to_json)
Stub Time-Consuming Methods
# Slow - actual file processing
it "processes file" do
result = FileProcessor.process(large_file)
expect(result).to be_present
end
# Fast - stub the slow method
it "processes file" do
allow(FileProcessor).to receive(:process).and_return({ success: true })
result = FileProcessor.process(large_file)
expect(result).to eq({ success: true })
end
Test Data Optimization
Use build_stubbed for Speed
# build_stubbed creates objects with:
# - Fake IDs (but not nil)
# - Fake timestamps
# - No database writes
user = build_stubbed(:user)
user.id # => 1001 (fake but present)
user.persisted? # => true (pretends to be saved)
Minimal Factory Attributes
# Slow - lots of associations
factory :order do
user
shipping_address
billing_address
coupon
association :items, count: 5
end
# Fast - minimal required attributes
factory :order do
status { "pending" }
total { 100 }
trait :with_user do
user
end
end
Avoid N+1 in Test Setup
# Slow - N+1 queries in setup
let(:users) { create_list(:user, 10) }
before do
users.each { |u| create(:post, user: u) }
end
# Better - batch operations
before do
users = create_list(:user, 10)
Post.insert_all(users.map { |u| { user_id: u.id, title: "Post" } })
end
CI Optimization
Fail Fast
# spec/spec_helper.rb
RSpec.configure do |config|
config.fail_fast = ENV["CI"].present?
end
Bisect for Flaky Tests
# Find minimal set that reproduces failure
rspec --bisect
Example Status Persistence
# spec/spec_helper.rb
RSpec.configure do |config|
# Re-run only failed specs first
config.example_status_persistence_file_path = "spec/examples.txt"
end
Performance Checklist
Quick Wins
- Use
buildinstead ofcreatewhen possible - Use
build_stubbedfor presentation tests - Stub external HTTP calls
- Avoid
let!whenletworks
Medium Effort
- Set up parallel testing
- Profile and fix slowest specs
- Minimize factory associations
- Use
before(:all)for read-only setup
Advanced
- Database cleaner strategy optimization
- Shared database connections for system specs
- Spring preloader for development
- CI caching for gems and assets
Benchmarking Helpers
# spec/support/benchmark_helper.rb
module BenchmarkHelper
def measure(label = "Block", &block)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = block.call
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
puts "#{label}: #{(elapsed * 1000).round(2)}ms"
result
end
end
# Usage in specs
include BenchmarkHelper
it "performs quickly" do
measure("User creation") { create(:user) }
measure("User build") { build(:user) }
end
Additional Resources
Reference Files
references/parallel-testing.md- Parallel test configurationreferences/ci-optimization.md- CI-specific optimizations
Example Files
examples/fast_spec.rb- Optimized spec patternsexamples/slow_spec.rb- Anti-patterns to avoid