RSpec Power π₯
A powerful collection of RSpec helpers and utilities that supercharge your Rails testing experience! π
β¨ Features
Feature | Summary | Usage |
---|---|---|
π Enhanced Logging | Capture and control Rails logs; ActiveRecord-only option |
:with_log , :with_logs , :with_log_ar , with_logging , with_ar_logging
|
π Environment Management | Override environment variables with auto-restore |
:with_env , with_test_env
|
π I18n Testing | Switch locales and assert translations |
:with_locale , with_locale
|
β° Time Freeze | Freeze/travel time for deterministic tests | :with_time_freeze |
π Time Zone | Run examples in a specific time zone | :with_time_zone |
β‘ Performance Budgeting | Enforce maximum example execution time |
with_maximum_execution_time , :with_maximum_execution_time
|
π Benchmarking | Run examples multiple times and summarize | with_benchmark: { runs: N } |
π CI Guards | Conditionally run or skip on CI |
:with_ci_only , :with_skip_ci
|
π§ͺ SQL Guards | Ensure no SQL or require at least one |
expect_no_sql , :with_no_sql_queries , expect_sql , :with_sql_queries
|
πΎ Request Dump | Dump session, cookies, flash, headers after each example |
:with_request_dump , with_request_dump: { what: [:session, :cookies, :flash, :headers] }
|
π’ DB Dump on Failure | Dump DB tables to CSV when an example fails |
:with_dump_db_on_fail , with_dump_db_on_fail: { tables: [...], except: [...] }
|
π¦ Installation
Add this line to your application's Gemfile:
group :test do
gem "rspec_power"
end
And then execute:
bundle install
If you are using the "timecop" gem, you need to
- add to Gemfile as
gem "rspec_power", require: false
- add
require "rspec_power"
beforerequire "timecop"
inrails_helper.rb
.
π Quick Start
The gem automatically configures itself when required. Just add it to your Gemfile.
π Usage Examples
π Enhanced Logging
Capture all Rails logs during specific tests, copied from test-prof gem with a small enhancement:
RSpec.describe User, :with_log do
it "creates a user with logging" do
# All Rails logs will be captured and displayed
user = User.create!(name: "John Doe", email: "john@example.com")
expect(user).to be_persisted
end
end
or for specific tests (alias :with_logs
is also supported):
RSpec.describe User do
it "creates a user with logging", :with_log do
# All Rails logs will be captured and displayed
user = User.create!(name: "John Doe", email: "john@example.com")
expect(user).to be_persisted
end
end
Capture only ActiveRecord logs:
RSpec.describe User, :with_log_ar do
it "shows SQL queries" do
# Only ActiveRecord logs will be captured
users = User.where(active: true).includes(:profile)
expect(users).to be_any
end
end
Manual logging control:
RSpec.describe User do
it "manually controls logging" do
with_logging do
# All Rails logs captured here
User.create!(name: "Jane")
end
# Logging back to normal here
end
it "captures only ActiveRecord logs" do
with_ar_logging do
# Only ActiveRecord logs captured here
User.count
end
end
end
π§ͺ SQL Guards
Ensure a block performs no SQL:
RSpec.describe CacheWarmup do
it "does not hit the DB" do
expect_no_sql do
CacheWarmup.build_in_memory!
end
end
end
Require that a block performs at least one SQL statement:
RSpec.describe MigrationChecker do
it "touches the DB" do
expect_sql do
ActiveRecord::Base.connection.execute("SELECT 1")
end
end
end
Or tag an example/group to enforce or require queries automatically:
RSpec.describe CacheWarmup, :with_no_sql_queries do
it "builds entirely in memory" do
CacheWarmup.build_in_memory!
end
end
RSpec.describe MigrationChecker, :with_sql_queries do
it "must hit the DB at least once" do
ActiveRecord::Base.connection.execute("SELECT 1")
end
end
Supported tags: `:with_no_sql_queries`, `:with_sql_queries`
π Environment Variable Management
Override environment variables for specific tests:
RSpec.describe PaymentService, :with_env do
it "uses test API key", with_env: { 'STRIPE_API_KEY' => 'test_key_123' } do
service = PaymentService.new
expect(service.api_key).to eq('test_key_123')
end
it "handles multiple env vars", with_env: {
'RAILS_ENV' => 'test',
'DATABASE_URL' => 'postgresql://localhost/test_db'
} do
expect(ENV['RAILS_ENV']).to eq('test')
expect(ENV['DATABASE_URL']).to eq('postgresql://localhost/test_db')
end
end
Manual environment control:
RSpec.describe ConfigService do
it "manually overrides environment" do
with_test_env('API_URL' => 'https://api.test.com') do
expect(ENV['API_URL']).to eq('https://api.test.com')
end
# Environment restored automatically
end
end
π Internationalization (I18n) Testing
Test your application in different locales:
RSpec.describe User, :with_locale do
it "displays name in English", with_locale: :en do
user = User.new(name: "John")
expect(user.greeting).to eq("Hello, John!")
end
it "displays name in Spanish", with_locale: :es do
user = User.new(name: "Juan")
expect(user.greeting).to eq("Β‘Hola, Juan!")
end
it "displays name in French", with_locale: :fr do
user = User.new(name: "Jean")
expect(user.greeting).to eq("Bonjour, Jean!")
end
end
Manual locale control:
RSpec.describe LocalizationHelper do
it "manually changes locale" do
with_locale(:de) do
expect(I18n.locale).to eq(:de)
expect(t('hello')).to eq('Hallo')
end
# Locale restored automatically
end
end
β° Time Freeze
Freeze time for consistent test results:
RSpec.describe Order, :with_time_freeze do
it "creates order with current timestamp", with_time_freeze: "2024-01-15 10:30:00" do
order = Order.create!(amount: 100)
expect(order.created_at).to eq(Time.parse("2024-01-15 10:30:00"))
end
it "handles time-sensitive logic", with_time_freeze: Time.new(2024, 12, 25, 12, 0, 0) do
expect(Time.current).to eq(Time.new(2024, 12, 25, 12, 0, 0))
# Test Christmas-specific logic
end
end
π Time Zone
RSpec.describe ReportGenerator do
it "builds report in US Pacific", with_time_zone: "Pacific Time (US & Canada)" do
# The block runs with Time.zone set to Pacific
expect(Time.zone.name).to eq("Pacific Time (US & Canada)")
end
end
β‘ Performance Budgeting
Limit example duration:
RSpec.describe Importer do
it "is fast enough" do
with_maximum_execution_time(50) do
Importer.run!
end
end
end
Or via metadata:
RSpec.describe Importer, with_maximum_execution_time: 100 do
it "completes quickly" do
Importer.run!
end
end
π Benchmarking
Benchmark entire examples via metadata and get a suite summary:
RSpec.describe Parser, with_benchmark: { runs: 10 } do
it "parses quickly" do
Parser.parse!(payload)
end
end
The example is executed multiple times (runs) and the average/min/max times are printed after the suite.
πΎ Request Dump
Dump request-related state after each example to help debug request specs.
Supported items:
:session
:cookies
:flash
:headers
Enable for an example or group and choose what to dump:
RSpec.describe "Users API", type: :request do
it "dumps everything by default", :with_request_dump do
post "/set_state"
expect(response).to be_successful
end
it "dumps only session and cookies",
with_request_dump: { what: [:session, :cookies] } do
post "/set_state"
expect(response).to be_successful
end
end
Example output:
[rspec_power] Dump after example: Users API dumps everything by default
[rspec_power] session: {"user_id"=>42}
[rspec_power] cookies: {"hello"=>"world"}
[rspec_power] flash: {"notice"=>"done"}
[rspec_power] headers: { ... }
π CI Guards
Run or skip specs depending on whether the suite is running on CI.
- Tag to run only on CI:
:with_ci_only
- Tag to skip on CI:
:with_skip_ci
RSpec.describe Deployment, :with_ci_only do
it "runs only on CI" do
expect(ENV["CI"]).to be_present
end
end
RSpec.describe HeavySpec, :with_skip_ci do
it "skips on CI" do
# expensive checks
end
end
CI detection via environment variable:
The guards rely on the CI
environment variable:
- Considered CI when
ENV["CI"]
is set to any non-empty value other than"false"
or"0"
(case-insensitive). - Considered non-CI when
ENV["CI"]
is unset/empty,"false"
, or"0"
.
Examples:
# Run a single file as if on CI
CI=true bundle exec rspec spec/path/to/file_spec.rb
# Also treated as CI
CI=1 bundle exec rspec
# Explicitly run as non-CI
CI=0 bundle exec rspec
π’ DB Dump on Failure
Dump database state to CSV files when an example fails. Useful to inspect exactly what data led to the failure.
- Defaults:
- Dumps all non-empty tables, excluding
schema_migrations
andar_internal_metadata
- Exports each table to a separate CSV, ordered by primary key (if present)
- Writes to
tmp/rspec_power/db_failures/<timestamp>_<spec-name>/
- Includes
metadata.json
with spec info
- Dumps all non-empty tables, excluding
Enable for an example or group:
RSpec.describe User, :with_dump_db_on_fail do
it "creates a user" do
# ...
end
end
Customize which tables to include/exclude and output directory:
RSpec.describe Report, with_dump_db_on_fail: {
tables: ["users", "accounts"],
except: ["accounts"],
dir: Rails.root.join("tmp", "db_dumps").to_s
} do
it "fails and dumps only selected tables" do
# ...
end
end
Options:
-
tables
/only
: whitelist tables to dump -
except
/exclude
: tables to skip -
dir
: base output directory (default:tmp/rspec_power/db_failures
)
Compatibility: the legacy tag :dump_db_on_fail
remains supported as an alias.
π― Shared Contexts
The gem provides several pre-configured shared contexts:
-
rspec_power::logging:verbose
- Enables verbose logging for tests with:with_log
metadata -
rspec_power::logging:active_record
- Enables ActiveRecord logging for tests with:with_log_ar
metadata -
rspec_power::env:override
- Automatically handles environment variable overrides -
rspec_power::i18n:dynamic
- Manages locale changes for tests with:with_locale
metadata -
rspec_power::time:freeze
- Handles time freezing for tests with:with_time_freeze
metadata -
rspec_power::time:zone
- Executes examples in a given time zone with:with_time_zone
metadata -
rspec_power::ci:only
- Runs examples only in CI when tagged with:with_ci_only
-
rspec_power::ci:skip
- Skips examples in CI when tagged with:with_skip_ci
-
rspec_power::request_dump:after
- Dumps selected request state after each example with:with_request_dump
metadata
π§ Configuration
The gem automatically configures itself, but you can customize the behavior:
# In spec_helper.rb or rails_helper.rb
RSpec.configure do |config|
# Customize logging behavior
config.include RSpecPower::Rails::LoggingHelpers
config.include_context "rspec_power::logging:verbose", with_log: true
config.include_context "rspec_power::logging:verbose", with_logs: true
config.include_context "rspec_power::logging:active_record", with_log_ar: true
# Customize environment helpers
config.include RSpecPower::Rails::EnvHelpers
config.include_context "rspec_power::env:override", :with_env
# Customize I18n helpers
config.include RSpecPower::Rails::I18nHelpers
config.include_context "rspec_power::i18n:dynamic", :with_locale
# Customize time helpers
config.include RSpecPower::Rails::TimeHelpers
config.include_context "rspec_power::time:freeze", :with_time_freeze
config.include_context "rspec_power::time:zone", :with_time_zone
# CI-only guards
config.include_context "rspec_power::ci:only", :with_ci_only
config.include_context "rspec_power::ci:skip", :with_skip_ci
# Request dump helpers (session/cookies/flash/headers)
config.include RSpecPower::RequestDumpHelpers
config.include_context "rspec_power::request_dump:after", :with_request_dump
end
π§ͺ Testing
Run the test suite:
bundle exec rspec
Linter
bundle exec rubocop
To fix most issues, run:
bundle exec rubocop -A
π€ Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
π Credits
Code for logging was extracted from test-prof gem.
π License
This project is licensed under the MIT License - see the MIT-LICENSE file for details.
Made with β€οΈ for the Rails testing community!