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.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide11 sections
  1. Make the Around Hook the Ownership Boundary
  2. Build a Fresh Driver From Example Metadata
  3. Put Capture and Quit in Separate Ensure Guards
  4. Expose the Driver Without a Global Registry
  5. Make Wait Blocks Express Domain Completion
  6. Understand Hook Ordering and Failure Visibility
  7. Keep Parallel Workers Fully Independent
  8. Analyze Lifecycle Failures in Order
  9. Accept the Cost of Example Isolation
  10. Review Hooks as Resource Code
  11. 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.

  1. 01 / rspec example

    RSpec Example

    Metadata selects a browser while the example remains the resource owner.

  2. 02 / around setup

    Around Hook Setup

    The hook builds options and assigns one driver before example.run.

  3. 03 / webdriver block

    WebDriver Block

    The example and page objects use the instance-scoped driver.

  4. 04 / example assertion

    Example Assertion

    RSpec records the primary failure before diagnostics begin.

  5. 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.

RUBY
# 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
end

Keep 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.

RUBY
# 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
end

When 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.

RUBY
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
end

Use 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.

RUBY
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
end

Ignore 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.run is 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.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

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.

  1. 01
    Selenium documentation

    Selenium Project

    Canonical WebDriver, Grid, waits, element, and browser automation guidance.

  2. 02
    WebDriver 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.