Managing Test State with RSpec around(:each) Hooks
Stop fighting leaky test databases. Learn how to use RSpec's around(:each) hook to wrap tests in transactions for faster, cleaner, and more reliable Rails test suites.
09 Mar 2026, 08:15 UTC

The Problem: Leaky Test Databases
A common frustration in Rails testing is the "leaky" test. This happens when a test creates a record in the database but fails to delete it before the next test runs. The resulting state pollution leads to intermittent failures—tests that pass in isolation but fail when run as a suite—making debugging a nightmare.
While before(:each) and after(:each) hooks can handle cleanup, they often require manual deletion logic or expensive truncation of every table. The more efficient approach is to wrap the entire test execution in a database transaction that never commits.
The Thesis: Why Use around(:each)?
The around(:each) hook allows you to wrap an example in a custom block of code. By wrapping example.run inside a database transaction, you ensure that every change made during the test is automatically rolled back, regardless of whether the test passed, failed, or crashed. This provides a clean slate for every single test without the overhead of deleting records manually.
Implementing Transactional Wrappers
In RSpec, the around hook receives the example object as an argument. You must explicitly call example.run to trigger the actual test logic. If you forget this call, the test will be marked as passed, but no assertions will actually execute.
For Rails applications, this is often implemented via the DatabaseCleaner gem or by configuring use_transactional_fixtures = true in RSpec. However, understanding how to implement this manually or via a custom hook gives you control over specific groups of tests that require unique cleanup strategies.
Worked Example: Custom Transaction Hook
The following configuration demonstrates how to implement a transactional wrapper in spec/rails_helper.rb. This example assumes you are using RSpec 3.x or 4.x and have a database adapter that supports transactions (like PostgreSQL or MySQL).
RSpec.configure do |config|
config.around(:each) do |example|
# Start a transaction
ActiveRecord::Base.transaction do
example.run
# Force a rollback regardless of the test outcome
raise ActiveRecord::Rollback
end
end
end
Execution Details:
- Where to run: Place this in your
rails_helper.rbor a dedicated support file inspec/support/. - Permissions: Requires the database user to have transaction privileges.
- Expected Result: Any
User.create!orPost.create!calls within your tests will be visible during the test run but will not exist in the database once the test completes. - Risk: If you use
example.runinside a block that catches all exceptions, you might accidentally swallow RSpec failure notifications.
Trade-offs and Limitations
While transactional hooks are fast and reliable for most unit and integration tests, they have specific limitations:
- External Processes: If your test triggers a background job (via Sidekiq or ActiveJob) or an external API callback that hits your database, those processes will not see the data inside the transaction. Because the transaction hasn't been committed, the separate process sees an empty database.
- Multi-Process Testing: When using tools like
parallel_tests, each process maintains its own transaction. You cannot use this hook to share state across different test processes. - Intentional Commits: You cannot use this strategy for tests specifically designed to verify
after_commithooks in Rails, as the commit never actually happens.
Verifying the Result
To verify that your around hook is working, create a test that intentionally fails after creating a record:
- Write a test that creates a record:
User.create!(email: '[contact removed]'). - Add a failing assertion:
expect(false).to be(true). - Run the test and then check your database via
rails console. - If the hook is working,
User.find_by(email: '[contact removed]')should returnnil, proving the rollback occurred despite the test failure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.