PRACTICAL GUIDE / Selenium Ruby RSpec driver lifecycle
Selenium Ruby Driver Lifecycles with RSpec Hooks and Blocks
Build Selenium Ruby driver lifecycles with RSpec around hooks, example-scoped ownership, resilient ensure cleanup, explicit waits, and parallel safety.
In this guide11 sections
- Make the Around Hook the Ownership Boundary
- Build a Fresh Driver From Example Metadata
- Put Capture and Quit in Separate Ensure Guards
- Expose the Driver Without a Global Registry
- Make Wait Blocks Express Domain Completion
- Understand Hook Ordering and Failure Visibility
- Keep Parallel Workers Fully Independent
- Analyze Lifecycle Failures in Order
- Accept the Cost of Example Isolation
- Review Hooks as Resource Code
- Let the Block Show the Whole Lifetime
What you will learn
- Make the Around Hook the Ownership Boundary
- Build a Fresh Driver From Example Metadata
- Put Capture and Quit in Separate Ensure Guards
- Expose the Driver Without a Global Registry
RSpec hooks can make a Selenium Ruby suite either disciplined or invisible. A driver created in before(:all) and kept in a class variable survives far beyond one example, couples failures, and becomes unsafe as soon as workers run concurrently. An around(:each) hook gives the resource a much clearer shape: acquire, run exactly one example, collect evidence, and release in ensure.
The hook is only the boundary. The driver must live on the current example instance, page objects must receive it explicitly, waits must return meaningful conditions, and parallel processes must use distinct data and artifact paths. Ruby's blocks make this lifecycle concise without making it implicit.
Make the Around Hook the Ownership Boundary
An RSpec around hook wraps the example and its inner per-example hooks. Code before example.run is setup; code after it is normal post-execution work; ensure is the release boundary that runs when setup after acquisition, the example, or later hook work raises. Call example.run once and only once.
The Selenium Ruby API exposes Selenium::WebDriver.for, browser options, waits, and session commands. RSpec determines lifetime. Keep the driver in @driver on the current example context rather than a class variable or process-global registry.
Animated field map
RSpec Example-Owned Browser
An around hook creates one driver for one example, lets block-based test code run, captures the result, and always reaches ensure cleanup.
01 / rspec example
RSpec Example
Metadata selects a browser while the example remains the resource owner.
02 / around setup
Around Hook Setup
The hook builds options and assigns one driver before example.run.
03 / webdriver block
WebDriver Block
The example and page objects use the instance-scoped driver.
04 / example assertion
Example Assertion
RSpec records the primary failure before diagnostics begin.
05 / ensure quit
Ensure Driver Quit
Capture and quit are guarded separately, then the reference is cleared.
Build a Fresh Driver From Example Metadata
Metadata is useful for declarative browser selection, but validate it centrally. A typo should fail as configuration rather than silently selecting a default browser. Build a new options object and driver for every example that carries the browser tag.
# spec/support/driver_factory.rb
require 'selenium-webdriver'
module DriverFactory
module_function
def build(browser:, grid_url: nil)
options = options_for(browser)
if grid_url && !grid_url.empty?
Selenium::WebDriver.for(
:remote,
url: grid_url,
capabilities: options
)
else
Selenium::WebDriver.for(browser, options: options)
end
end
def options_for(browser)
case browser
when :chrome
Selenium::WebDriver::Options.chrome(args: ['--headless', '--lang=en-US'])
when :firefox
options = Selenium::WebDriver::Options.firefox(args: ['-headless'])
options.add_preference('intl.accept_languages', 'en-US')
options
else
raise ArgumentError, "Unsupported browser: #{browser.inspect}"
end
end
endKeep credentials and Grid URLs in environment-backed configuration, not example metadata that reports may print. Browser metadata should express a finite supported choice. If tests need locale, proxy, or download behavior, model those keys explicitly and create a new options instance each time.
Put Capture and Quit in Separate Ensure Guards
The hook should make @driver available only inside the example lifecycle. After example.run, RSpec exposes example.exception when execution failed. Capture while the session is alive, but isolate that optional operation from mandatory cleanup.
# spec/support/selenium_lifecycle.rb
require 'fileutils'
require_relative 'driver_factory'
RSpec.configure do |config|
config.around(:each, :browser) do |example|
@driver = nil
begin
browser = example.metadata.fetch(:browser)
@driver = DriverFactory.build(
browser: browser,
grid_url: ENV['SELENIUM_GRID_URL']
)
example.run
ensure
if @driver && example.exception
begin
safe_id = example.id.gsub(/[^A-Za-z0-9_.-]+/, '_')
directory = File.join('artifacts', Process.pid.to_s, safe_id)
FileUtils.mkdir_p(directory)
@driver.save_screenshot(File.join(directory, 'failure.png'))
File.write(File.join(directory, 'page-source.html'), @driver.page_source)
rescue StandardError => capture_error
warn "Selenium artifact capture failed: #{capture_error.class}: #{capture_error.message}"
end
end
begin
@driver&.quit
rescue StandardError => quit_error
warn "Selenium quit failed: #{quit_error.class}: #{quit_error.message}"
raise unless example.exception
ensure
@driver = nil
end
end
end
endWhen the example already failed, the code reports a quit failure without replacing the primary exception. When the example passed, failed cleanup raises so a leaked session does not look healthy. A project reporter can attach secondary exceptions more formally; the ordering principle remains the same.
If driver construction itself raises, @driver remains nil and cleanup is harmless. If later setup inside the begin raises, the acquired driver still reaches ensure. Assign ownership immediately after construction before performing navigation or account setup.
Expose the Driver Without a Global Registry
Example code needs a clear accessor that fails outside the lifecycle. Define it as an instance helper included in relevant examples. Do not use $driver, a constant, a class variable, or a singleton because parallel examples in one process would share that location.
module BrowserExample
def driver
@driver || raise('WebDriver is outside the current example lifecycle')
end
end
RSpec.configure do |config|
config.include BrowserExample, type: :system
end
RSpec.describe 'Documentation search', browser: :chrome, type: :system do
it 'shows a matching result' do
driver.navigate.to('https://www.selenium.dev/documentation/')
driver.find_element(css: 'input[type="search"]').send_keys('WebDriver')
wait = Selenium::WebDriver::Wait.new(timeout: 10)
result = wait.until do
element = driver.find_element(css: '[data-search-result]')
element if element.displayed?
end
expect(result.text).to include('WebDriver')
end
endUse the tag consistently: the around hook in the example is selected by :browser, while helper inclusion is selected by type: :system. A shared context can apply both if the suite wants one marker. What matters is that an example cannot call driver unless its lifecycle hook created one.
Make Wait Blocks Express Domain Completion
Selenium::WebDriver::Wait#until repeatedly evaluates a block until it returns a truthy value or times out. Return the useful object or domain value only when the condition is satisfied. Re-find elements inside the block when the page replaces them; holding an element from before a render can produce stale references.
def wait_for_order_status(driver, expected)
wait = Selenium::WebDriver::Wait.new(
timeout: 12,
interval: 0.2,
ignored: [Selenium::WebDriver::Error::StaleElementReferenceError]
)
wait.until do
status = driver.find_element(css: '[data-testid="order-status"]')
status if status.text == expected
end
endIgnore only a transient exception that the condition is designed to recover from. Broadly rescuing StandardError inside the block hides invalid selectors, lost sessions, and application failures until a vague timeout. Configure timeout and interval from suite policy rather than copying one value into every page object.
Blocks also help page objects return domain state. A method can wait for a confirmation element and return its text; the example then asserts meaning rather than coordinating low-level polling.
Understand Hook Ordering and Failure Visibility
An around(:each) hook must call example.run for RSpec to execute inner before, the example body, and after hooks. If browser setup belongs outside example.run, inner before hooks can use the driver. The surrounding ensure still runs after inner teardown.
Do not use return from inside ensure. In Ruby, that can suppress an exception and turn a failed example into misleading control flow. Avoid rescuing the whole lifecycle unless you re-raise the primary error. Narrow rescue blocks around diagnostics and cleanup are easier to audit.
An after(:each) hook can also quit a driver, but construction in a separate before(:each) hook spreads ownership across callbacks and makes partial setup harder to see. The around hook keeps acquire and release lexically paired. Use suite-level hooks for services that truly belong to the suite, not for browser sessions intended to be fresh.
Keep Parallel Workers Fully Independent
RSpec core does not turn an instance variable into cross-process state. Parallel runners may fork processes or start separate processes, each with its own memory. That protects @driver between processes, but shared accounts, ports, files, and Grid slots still collide.
Include process or worker identity plus example.id in artifact paths. Allocate unique application users or data leases. Never cache a driver in a class variable to save startup within a worker; all examples in that worker can then inherit cookies, windows, and a broken session.
The Selenium advice to avoid sharing state and to use a fresh browser per test applies regardless of the Ruby parallelization tool. Bound worker count to matching Grid capacity and application load limits.
Analyze Lifecycle Failures in Order
If an example reports no driver, verify that its metadata selected the around hook and that construction completed before the accessor ran. If one example sees another's page, search for class variables, constants, global variables, cached page objects, or a wider-scoped hook. An instance variable is safe only when code does not copy it into shared objects.
If sessions leak after failures, inspect whether capture raised before quit and whether the quit block was independently guarded. If the original assertion vanishes behind a quit error, preserve example.exception and report cleanup as secondary. If artifacts overwrite one another, add process, example, parameter, and retry identity to the path.
Wait timeouts deserve separate diagnosis: inspect the final DOM and condition, not just the elapsed time. A block that always returns an element, even when text is wrong, ends too early; a block that rescues every exception hides the true cause.
Accept the Cost of Example Isolation
A fresh browser per example has startup cost, but it localizes state and lets examples run in any order. Reusing a driver in before(:context) saves startups while making the group serial, coupling outcomes, and requiring a reset strategy for windows, storage, downloads, and application data.
If a business journey must span several steps, model it as one example with meaningful internal reporting rather than several examples that secretly require order. The browser lifecycle then still matches one result.
Review Hooks as Resource Code
- One
around(:each)hook owns creation,example.run, evidence, and release. example.runis called exactly once.- Driver ownership uses an example instance variable, never process-global storage.
- Construction assigns ownership before other setup can fail.
- Capture and quit have separate rescue paths inside
ensure. - A quit failure cannot erase an existing example exception.
- Wait blocks return truthy values only for completed domain conditions.
- Only understood transient exceptions are ignored during waits.
- Artifact and test-data identities remain unique across workers and retries.
- Worker count fits the browser matrix and Grid admission capacity.
Let the Block Show the Whole Lifetime
The best RSpec Selenium lifecycle is visible in one block: create the driver, run one example, inspect its result, and release the session in ensure. Keep state on that example, keep waits specific, and keep parallel workers independent. Ruby's concise hooks then clarify ownership instead of concealing it.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Selenium documentation
Selenium Project
Canonical WebDriver, Grid, waits, element, and browser automation guidance.
- 02WebDriver standard
W3C
The browser automation protocol specification behind WebDriver implementations.
FAQ / QUICK ANSWERS
Questions testers ask
Why use an RSpec around hook for Selenium WebDriver?
An around hook places construction, example execution, failure capture, and cleanup in one lexical lifecycle. Its ensure block runs when the example or inner hooks fail.
Must an RSpec around hook call example.run?
Yes, exactly once for a normal example lifecycle. Omitting it skips the example; calling it more than once repeats hooks and test behavior against ambiguous state.
Is an instance variable driver safe in RSpec parallel runs?
It is safe when it belongs to the current example group instance and each parallel worker owns its own example lifecycle. Class variables, constants, and process-global caches are shared.
How should Ruby ensure handle screenshot and quit failures?
Guard capture and quit separately inside ensure. Record secondary errors, clear the reference, and never let screenshot failure prevent the attempt to quit the owned session.
What should a Selenium Ruby wait block return?
Return a truthy value only when the domain condition is satisfied. Re-find dynamic elements inside the block and ignore only narrowly understood transient exceptions.
RELATED GUIDES
Continue the learning route
GUIDE 01
Selenium Grid Tutorial: Run Tests Across Browsers
Selenium Grid tutorial explaining architecture, setup, remote WebDriver, browser capabilities, parallel execution, Docker, CI, and debugging tips.
GUIDE 02
Selenium Wait Commands: Implicit, Explicit, and Fluent Waits
Selenium wait commands explained with implicit, explicit, and fluent waits, practical examples, timing mistakes, flake fixes, and stable patterns.
GUIDE 03
Designing a Cross-Browser Options Matrix in Selenium WebDriver 4
Design a Selenium browser options matrix that separates W3C capabilities, vendor settings, environments, and verified cross-browser expectations.
GUIDE 04
Instrument Selenium Commands with WebDriver Listeners
Instrument Selenium commands with WebDriverListener, structured timing events, failure screenshots, safe redaction, and useful execution timelines.