spec-performance

bastos's avatarfrom bastos

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.

0stars🔀0forks📁View on GitHub🕐Updated Jan 10, 2026

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.
nameSpec Performance
descriptionThis 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.
version1.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 build instead of create when possible
  • Use build_stubbed for presentation tests
  • Stub external HTTP calls
  • Avoid let! when let works

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 configuration
  • references/ci-optimization.md - CI-specific optimizations

Example Files

  • examples/fast_spec.rb - Optimized spec patterns
  • examples/slow_spec.rb - Anti-patterns to avoid