Skip to main content
Back to Blog
Tutorial
2026-09-28

SeleniumBase Guide: Python Browser Testing with pytest

seleniumbase guide for QA engineers: write pytest browser tests, choose BaseCase or sb fixtures, run CI, reports, UC mode, and debug flakes.

SeleniumBase Guide: Python Browser Testing with pytest

SeleniumBase is an active Python browser automation framework built on top of Selenium, pytest, and related tooling. The current package release I verified on PyPI is 4.54.12, released on September 25, 2026, with Python >=3.10 listed as the requirement. The project has not been renamed or discontinued. Its current docs still present BaseCase, the sb pytest fixture, the SB() context manager, Driver(), CDP mode, UC mode, Recorder, dashboards, and pytest reports as supported ways to work.

The direct answer for QA teams: use SeleniumBase when raw Selenium is too repetitive but you still want Selenium-compatible browser automation in Python. It gives you automatic waits, cleaner assertions, a pytest plugin, browser selection flags, screenshots on failure, dashboards, reports, reusable sessions, Recorder workflows, and stealth-oriented modes for specific cases. It is not a reason to skip test design. Bad selectors, unclear assertions, shared state, and CI browser drift can still make a SeleniumBase suite unreliable.

This guide focuses on pytest because that is the main shape most automation engineers will use with AI coding agents such as Claude Code, Cursor, and Copilot. If you are still deciding how Python, pytest, and Selenium fit together, read the Selenium Python pytest integration complete guide. If you need a beginner path before adopting a framework layer, start with the Selenium Python tutorial 2026.

Install, Verify, and Pin the Runtime

SeleniumBase installs from PyPI with pip install seleniumbase. The official package page also advertises optional extras and project links, but for a test repository you should pin the dependency in a constraints file or a lockfile and verify the command-line tool in CI. SeleniumBase exposes both seleniumbase and sbase console commands, including helpers such as sbase get cft, sbase get chs, sbase options, and sbase recorder.

python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install seleniumbase==4.54.12
sbase options
pytest --version

Use a modern Python version supported by your organization. PyPI lists Python >=3.10, and the classifiers include current Python 3 releases. In practice, most CI examples should use Python 3.12 or 3.13 unless your application stack forces a narrower choice. The browser side is just as important: Chrome, Chromium, Chrome for Testing, Firefox, Edge, and Safari all imply different local and CI setup.

LayerRecommended controlReason
PythonPin in CI, for example 3.13Keeps pytest plugins and dependencies stable
SeleniumBasePin seleniumbase==4.54.12 during adoptionPrevents silent framework changes while tests are being ported
BrowserChoose with --browser, --chrome, --edge, or --firefoxMakes cross-browser failures explicit
Browser binaryUse sbase get cft, sbase get chromium, or system browserAvoids driver mismatch surprises
ReportsUse --dashboard and --html=report.htmlGives reviewers artifacts beyond log text

For AI-generated test changes, make dependency pinning part of the task. Otherwise an agent can write code that passes on its own machine because a transitive package changed, while your CI worker still uses the previous lockfile.

BaseCase or sb Fixture

SeleniumBase documents many syntax formats, but pytest teams usually choose between BaseCase inheritance and the sb fixture. BaseCase is a good default when you want SeleniumBase to own setup, teardown, and test methods. The sb fixture is better when your suite already has pytest fixtures for users, tenants, API clients, database state, or feature flags.

FormatWhat it looks likeBest fitTradeoff
BaseCaseClass inherits BaseCaseNew SeleniumBase suite, simple page testsLess natural when composing many pytest fixtures
sb fixtureTest function accepts sbExisting pytest suite or fixture-heavy testsRequires passing sb into helpers
SB() context managerPlain Python context blockScripts, one-off checks, non-pytest flowsPytest reports and dashboard features are not the center
Driver()Improved raw driver objectMigration from raw SeleniumSome SeleniumBase pytest features are absent
CDP or UC modesSpecialized Chromium controlAnti-bot or stealth-sensitive diagnosticsNeeds careful ethical and reliability boundaries

A minimal BaseCase test looks like this. The BaseCase.main(__name__, __file__) line lets the file route itself through pytest if someone runs it directly with Python.

from seleniumbase import BaseCase

BaseCase.main(__name__, __file__)


class TestDemoPage(BaseCase):
    def test_demo_page_submit_changes_content(self):
        self.open("https://seleniumbase.io/demo_page")
        self.assert_text("Automation Practice", "h3")
        self.assert_element("input#myTextInput")
        self.type("input#myTextInput", "qa engineer")
        self.click("button#myButton")
        self.assert_element("tbody#tbodyId")
        self.assert_no_js_errors()

The same style with the sb fixture is more composable. This is the pattern I prefer when an AI coding agent is adding tests to a mature pytest repository, because the test can accept ordinary fixtures next to sb.

from seleniumbase import BaseCase


def test_login_round_trip_uses_visible_state(sb: BaseCase):
    sb.open("https://seleniumbase.io/simple/login")
    sb.type("#username", "demo_user")
    sb.type("#password", "secret_pass")
    sb.click('a:contains("Sign in")')
    sb.assert_exact_text("Welcome!", "h1")
    sb.assert_element("img#image1")
    sb.click_link("Sign out")
    sb.assert_text("signed out", "#top_message")

What people get wrong: they wrap SeleniumBase calls in extra explicit waits copied from raw Selenium. SeleniumBase methods already wait for visibility and interactability in common actions. Extra sleeps and duplicate waits often hide the actual readiness signal. Prefer a meaningful assertion after the action: text changed, an element appears, a URL contains an expected path, or a user-visible state is present.

Command-Line Flags That Matter in Real Suites

SeleniumBase's pytest plugin adds CLI options for browser choice, headless mode, dashboard, demo mode, UC mode, recorder mode, reuse-session mode, proxy settings, mobile mode, and more. Official examples include --headless, --browser, --uc, --demo, --dashboard, --html=report.html, --rs or --reuse-session, --crumbs, --reruns, --server, --port, --proxy, --mobile, and --metrics.

GoalCommandNotes
Local visible debuggingpytest tests/test_login.py --demoSlows and highlights actions so a human can follow
Headless CI runpytest tests --headless --browser=chromeUse a real browser target instead of leaving it implicit
Shared browser per workerpytest tests --rs --crumbsReuses a session but clears cookies between tests
SeleniumBase dashboardpytest tests --dashboard --html=report.htmlProduces dashboard.html and a pytest HTML report
Filter pytest testspytest tests -k loginNative pytest selection, useful for agent debugging
UC mode smokepytest tests/test_guarded.py --ucAvoid mixing with normal functional coverage

Put common defaults in pytest.ini, but keep environment-specific flags in CI commands. A local developer may want headed Chrome and demo mode. CI should usually run headless with reports and artifacts.

[pytest]
testpaths = tests
python_files = test_*.py *_test.py *_tests.py
addopts =
    -ra
    --strict-markers
markers =
    smoke: fast browser checks for pull requests
    checkout: payment and order-flow coverage
    guarded: tests that touch bot-sensitive pages

Then run focused slices without changing source code.

Keep selection conventions boring and stable. A marker such as smoke should mean "safe for every pull request", not "whatever the last engineer wanted to run quickly." A keyword filter such as -k login is excellent for diagnosis, but it should not become the permanent CI contract because test names change more often than business risk categories. When an AI coding agent adds tests, ask it to add or reuse a marker only if the marker already has a clear definition in pytest.ini. That keeps CI behavior reviewable and prevents a useful local debugging filter from becoming a hidden release gate.

pytest tests -m smoke --headless --browser=chrome --dashboard --html=report.html
pytest tests -k login --demo --browser=chrome
pytest tests/test_checkout.py -q --headless --reruns=1 --reruns-delay=1

Use --reruns sparingly. A retry can reduce noisy failures during a migration, but it can also normalize a broken wait or an order-dependent fixture. If a test needs retries every day, quarantine it, diagnose it, and remove the retry after the root cause is fixed.

Assertions That Survive UI Change

SeleniumBase makes browser actions concise, so the main quality lever becomes assertions. A weak test clicks through a page and asserts that the final status code is not an exception. A strong test proves the user-visible state changed and that the application did not quietly produce JavaScript errors.

Weak patternBetter SeleniumBase assertionWhy it is stronger
Click and hopesb.assert_text("Welcome!", "h1")Verifies visible state after the action
Compare location immediatelysb.assert_url_contains("/account") after a visible post-login checkAvoids racing navigation
Count elements without contextsb.assert_element("div.inventory_list") plus product textProves the expected region loaded
Sleep after every clicksb.assert_element('button:contains("Checkout")')Waits for the actual UI contract
Ignore frontend errorssb.assert_no_js_errors()Catches client-side regressions that leave DOM checks green

Here is a pattern that keeps page helpers small and assertions in the test. The helper performs a reusable action, while the test still declares the business outcome.

from seleniumbase import BaseCase


class LoginPage:
    def sign_in(self, sb: BaseCase, username: str, password: str) -> None:
        sb.open("https://seleniumbase.io/simple/login")
        sb.type("#username", username)
        sb.type("#password", password)
        sb.click('a:contains("Sign in")')


def test_successful_login_shows_welcome_and_allows_logout(sb: BaseCase):
    LoginPage().sign_in(sb, "demo_user", "secret_pass")

    sb.assert_exact_text("Welcome!", "h1")
    sb.assert_element("img#image1")
    sb.click_link("Sign out")
    sb.assert_text("signed out", "#top_message")
    sb.assert_no_js_errors()

This shape is agent-friendly. If Cursor or Copilot adds a new test, it can reuse LoginPage().sign_in(...) but still has to assert the user-facing state that makes that test valuable.

Reports, Logs, Screenshots, and Dashboards

SeleniumBase creates failure logs in latest_logs/ by default. The reports docs list files such as basic_test_info.txt, page_source.html, and screenshot.png for failures. The --dashboard flag generates dashboard.html, and --html=report.html creates a pytest HTML report. The docs also state that combining dashboard usage with pytest HTML can add dashboard information to the HTML report, and if the paths match, the dashboard can become an advanced HTML report after completion.

A practical CI command for a pull request is:

pytest tests \
  --headless \
  --browser=chrome \
  --dashboard \
  --html=report.html \
  --self-contained-html \
  -m smoke

The shell line continuations above are written for POSIX shells. If your CI runner is Windows PowerShell, rewrite it as one line or use PowerShell continuation syntax. Do not ask an agent to blindly convert shell snippets across shells without running them.

Here is a GitHub Actions workflow using the current action majors specified for this repository's article standards.

name: seleniumbase-browser-tests

on:
  pull_request:
  push:
    branches:
    - main

jobs:
  browser-tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v7
    - uses: actions/setup-python@v7
      with:
        python-version: '3.13'
        cache: pip
    - name: Install test dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install seleniumbase==4.54.12
    - name: Run SeleniumBase smoke tests
      run: |
        pytest tests --headless --browser=chrome --dashboard --html=report.html --self-contained-html -m smoke
    - name: Upload SeleniumBase artifacts
      if: always()
      uses: actions/upload-artifact@v7
      with:
        name: seleniumbase-artifacts-${{ github.run_id }}
        path: |
          report.html
          dashboard.html
          latest_logs/

The important part is if: always() on artifact upload. A failed browser test is exactly when the screenshot, page source, and report matter.

UC Mode, CDP Mode, and Ethical Boundaries

SeleniumBase UC Mode is designed for stealthier Chromium automation. The UC Mode docs explain that services can detect ChromeDriver while it is connected, and SeleniumBase uses underlying Selenium capabilities to disconnect and reconnect as needed. The docs also warn that UC Mode is detectable in headless mode, so do not combine those options and expect stealth.

For QA teams, UC Mode should be a targeted diagnostic tool, not the default way to test your own application. If your app blocks automation on staging because a bot rule is too aggressive, a UC-mode smoke test can help you identify the integration behavior. But your normal regression suite should test product behavior under normal browsers. Otherwise you risk proving that the workaround works rather than proving that users can complete the workflow.

from seleniumbase import SB


def main() -> None:
    with SB(uc=True, test=True, locale="en") as sb:
        sb.open("https://seleniumbase.io/simple/login")
        sb.type("#username", "demo_user")
        sb.type("#password", "secret_pass")
        sb.click('a:contains("Sign in")')
        sb.assert_exact_text("Welcome!", "h1")


if __name__ == "__main__":
    main()

That script is useful as a one-off check, but pytest remains better for suite reporting. Keep UC tests marked separately:

import pytest
from seleniumbase import BaseCase


@pytest.mark.guarded
def test_guarded_login_page_loads_in_uc_mode(sb: BaseCase):
    sb.open("https://seleniumbase.io/simple/login")
    sb.assert_element("#username")
    sb.assert_element("#password")

Then run them intentionally:

pytest tests -m guarded --uc --browser=chrome --dashboard --html=guarded-report.html

Recorder Mode and Agent-Assisted Test Creation

SeleniumBase includes Recorder tooling that can record browser actions and export automation scripts. Official docs reference sbase mkfile TEST_NAME.py --rec, sbase recorder, and related commands. Recorder output is useful for discovery, especially when a product flow is unfamiliar, but it should not be merged unreviewed. Recorded scripts tend to reflect every click and incidental selector, while durable tests express intent.

Use this workflow with an AI coding agent:

StageHuman or tool actionMerge criteria
RecordCapture the happy path with RecorderOutput is treated as draft code
RefactorAgent extracts page helpers and removes incidental waitsSelectors are readable and stable
AssertQA engineer adds domain assertionsTest proves state change, not only navigation
RunCI executes headless with report artifactsFailure evidence is uploaded
ReviewDiff is checked for overbroad selectors and secretsNo credentials, no hardcoded personal data

An agent prompt that works well: "Convert this recorded SeleniumBase script into pytest using the sb fixture. Keep the login helper small. Replace sleeps with visible assertions. Add one negative assertion that proves the failure banner appears for invalid credentials." That prompt names the design, not just the syntax.

A Real Failure Mode: Passing Locally, Failing in CI

A common SeleniumBase failure goes like this: tests pass on a laptop in visible Chrome, but CI fails with ElementNotVisibleException or a timeout on the first click. The cause is rarely "SeleniumBase is flaky." More often it is one of these differences.

SymptomLikely causeFix
Element exists but cannot be clickedCI viewport is smaller than local browserSet --window-size=1366,768 or design a mobile-specific assertion
Login fails only in CIEnvironment URL, credentials, or bot rule differsPrint the target base URL safely and mark guarded tests separately
Reports missing after failureArtifact upload skipped because job failedUse if: always() for report upload
Firefox fails but Chrome passesBrowser-specific UI or timing pathAdd browser-specific project decision, do not hide it with retries
UC mode works headed but fails headlessUC docs warn against headless stealth assumptionsRun UC headed where appropriate or avoid UC for CI gates

The diagnosis sequence should be mechanical. First, upload latest_logs/, dashboard.html, and report.html. Second, inspect page_source.html and screenshot before changing selectors. Third, reproduce the same command locally, including --headless, --browser, and --window-size. Fourth, change the assertion to wait for the actual user-facing state instead of adding a sleep.

pytest tests/test_login.py \
  --headless \
  --browser=chrome \
  --window-size=1366,768 \
  --dashboard \
  --html=report.html \
  -vv

If that command passes locally but fails in CI, the remaining suspects are environment, browser binary, network access, secrets, or service-side rules. At that point, changing SeleniumBase code is usually the wrong first move.

Frequently Asked Questions

Is SeleniumBase a replacement for raw Selenium?

SeleniumBase can replace much of the repetitive raw Selenium code in a pytest suite, especially waits, element lookup boilerplate, driver setup, screenshots, and reports. It still uses Selenium and WebDriver concepts underneath, so browser behavior, grid configuration, and selector quality still matter. Treat it as a productive test framework layer, not a magic browser. Teams that already have raw Selenium tests can migrate gradually by introducing Driver(), BaseCase, or the sb fixture in new tests first.

Should I use BaseCase or the sb fixture?

Use BaseCase when starting a fresh SeleniumBase suite and you want a clear class-based style. Use the sb fixture when your tests already depend on pytest fixtures for users, data setup, APIs, or feature flags. The fixture style is usually easier for mature automation repositories because it composes with existing pytest patterns. The most important choice is consistency: do not mix every supported SeleniumBase syntax in the same folder unless there is a clear reason.

Can SeleniumBase UC mode bypass all bot detection?

No. UC Mode is a specialized Chromium automation mode that can reduce some automation signals, and SeleniumBase documents how it disconnects and reconnects ChromeDriver for stealth-sensitive interactions. It is not a universal guarantee, and the docs warn that UC Mode is detectable in headless mode. QA teams should use it for targeted diagnostics or guarded flows, not as the default regression path. For your own application, normal browser tests are usually the more trustworthy signal.

What artifacts should CI save for SeleniumBase failures?

Always save latest_logs/, report.html, and dashboard.html when those files exist. Failure logs can include screenshots, page source, and basic test information, which are far more useful than a timeout stack trace alone. In GitHub Actions, put artifact upload behind if: always() so failed test jobs still publish evidence. If your suite runs in parallel, name artifacts with the run ID or matrix browser so reviewers can match failures to the correct environment.