VLearnVibium

How to Use Vibium with pytest

Use Vibium with pytest: wrap the browser in a fixture, write assertions, isolate state per test, capture screenshots on failure, and run headless in CI.

By Pramod Dutta··15 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

To use Vibium with pytest, wrap the browser in a pytest fixture that launches Vibium, yields a page to your test, and quits afterward — no special plugin required. Vibium is AI-native browser automation built on WebDriver BiDi and shipped as a single Go binary, created by Selenium and Appium co-creator Jason Huggins. Because its Python client exposes a clean synchronous API (from vibium import browser_sync as browser), it drops straight into pytest's fixture model. You define one vibe fixture in conftest.py, and every test receives a ready-to-drive page. Your tests then read like plain Python: vibe.find("#email").type("a@b.com"), then assert vibe.find("h1").text() == "Welcome". Vibium's find() auto-waits for actionability, so you get stable assertions without sleep(). Add a fixture that screenshots on failure, isolate each test with a fresh browser context, and flip headless with an environment variable, and you have a fast, CI-ready browser test suite that uses everything pytest already gives you — parametrize, markers, and -n auto parallelism.

Why pair Vibium with pytest at all?

pytest is the assertion and orchestration layer; Vibium is the browser. Vibium drives Chrome and finds elements, but it deliberately ships no test runner, no assertion library, and no reporting — that is pytest's job. Pairing them gives you fixtures for setup and teardown, assert for verification, parametrize for data-driven cases, markers for grouping, and a huge plugin ecosystem (xdist, html reports, reruns), all wrapping Vibium's automation.

This separation is a feature. You are not locked into a bespoke test framework: you use idiomatic pytest and treat Vibium like any other library your tests call. If you already know pytest, the only new thing to learn is Vibium's small, readable command set. New to Vibium itself? Start with what is Vibium and install Vibium.

How do I install Vibium and pytest?

Install both from PyPI into a virtual environment, then let Vibium fetch its own Chrome. Vibium is free (pip install vibium) and bundles its WebDriver BiDi engine into one binary, so there is no driver to match against a browser version.

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install vibium pytest
vibium install                   # pre-download Chrome for Testing (optional)

vibium install pre-fetches Chrome for Testing so your first test does not pay the download cost. If you skip it, Vibium downloads Chrome automatically on the first browser.launch(). That is the entire setup — no webdriver-manager, no PATH surgery, no browser/driver alignment.

What does the core browser fixture look like?

Put a single vibe fixture in conftest.py so every test file can request a fresh page by name. This is the one piece of glue between pytest and Vibium, and it centralizes launch and cleanup.

# conftest.py
import os
import pytest
from vibium import browser_sync as browser
 
 
@pytest.fixture
def vibe():
    headless = os.getenv("HEADLESS", "false") == "true"
    instance = browser.launch(headless=headless)
    yield instance
    instance.quit()

Everything before yield is setup (launch Chrome); everything after is teardown (quit() closes the browser even if the test fails). Reading headless from an environment variable means the same fixture runs headed on your laptop and headless in CI — one switch, no branching. A test then just asks for vibe:

# test_home.py
def test_homepage_title(vibe):
    vibe.go("https://example.com")
    assert vibe.find("h1").text() == "Example Domain"

pytest sees the vibe parameter, runs the fixture, and injects the launched browser. No boilerplate in the test body — that is the whole point of fixtures.

How do I write assertions against the page?

Use plain assert with Vibium's state-reading methods — text(), value(), is_visible(), attr(), count(). Vibium returns raw Python values, so pytest's assertion rewriting gives you rich failure output for free, no special matchers needed.

def test_login_success(vibe):
    vibe.go("https://app.example.com/login")
    vibe.find(label="Email").type("user@example.com")
    vibe.find(label="Password").type("secret123")
    vibe.find(role="button", text="Sign in").click()
 
    # Assertions read straight off the page
    assert vibe.find("h1").text() == "Dashboard"
    assert vibe.find(".welcome").is_visible()
    assert vibe.find(".cart-count").text() == "0"

Every find() here auto-waits for the element to be actionable — visible, stable, enabled — before typing, clicking, or reading, so you assert against a settled page instead of racing it. That is why there are no sleep() calls. For the full end-to-end login pattern, see automate login with Vibium; for the selector strategies used above, see find element.

Prefer semantic selectors (role, text, label, testid) over CSS paths so a styling change does not break your assertions. The companion guide selector best practices covers the priority order in depth.

How do I isolate state between tests with fixtures?

Give each test its own browser context so cookies, storage, and sessions never leak. A context is an isolated cookie jar and storage sandbox living inside one browser process — the cheap way to guarantee that a test does not pass only because a previous test logged in.

# conftest.py
import os
import pytest
from vibium import browser_sync as browser
 
 
@pytest.fixture(scope="session")
def bro():
    headless = os.getenv("HEADLESS", "false") == "true"
    instance = browser.launch(headless=headless)
    yield instance
    instance.quit()
 
 
@pytest.fixture
def vibe(bro):
    ctx = bro.new_context()     # fresh cookies + storage per test
    page = ctx.page()
    yield page
    ctx.close()

Here the browser launches once per session (fast), while each test gets a brand-new context (isolated). A test that seeds a cookie cannot poison the next one:

def test_starts_logged_out(vibe):
    vibe.go("https://app.example.com/account")
    assert vibe.find(role="button", text="Log in").is_visible()

Seeding auth by setting cookies on the context also lets you skip slow, flake-prone UI logins entirely — set the session cookie, navigate, and assert. This is the single biggest lever for a stable suite, and it is covered further in flake-free tests.

How do I capture a screenshot when a test fails?

Wire a pytest hook to the fixture so a failing test automatically saves a PNG of what the browser showed. Because CI runs are headless and remote, a failure screenshot is the fastest way to see the actual page state. This takes two small pieces.

First, a hook in conftest.py that records each test's phase outcome onto the node:

# conftest.py
import pytest
 
 
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, "rep_" + rep.when, rep)

Then the fixture checks that outcome after yield and screenshots on failure. Vibium's screenshot() returns PNG bytes, so writing it to disk is one line:

# conftest.py
import os
import pytest
from vibium import browser_sync as browser
 
 
@pytest.fixture
def vibe(request):
    headless = os.getenv("HEADLESS", "false") == "true"
    instance = browser.launch(headless=headless)
    yield instance
    if getattr(request.node, "rep_call", None) and request.node.rep_call.failed:
        png = instance.screenshot(full_page=True)
        with open(f"fail-{request.node.name}.png", "wb") as f:
            f.write(png)
    instance.quit()

Now every failed test drops a fail-<testname>.png next to your tests, and a passing test writes nothing. For a richer timeline you can record a Vibium trace instead and open it in the trace viewer. The capture options for screenshot() — full page, clip region — are documented in the screenshot command.

How do I write data-driven tests with parametrize?

Use @pytest.mark.parametrize to run one Vibium test body against many inputs. This is where the pytest pairing pays off: you write the browser flow once and pytest multiplies it into a separate reported case per row, each with its own isolated vibe fixture.

import pytest
 
CASES = [
    ("laptop", 12),
    ("phone", 30),
    ("headphones", 18),
]
 
 
@pytest.mark.parametrize("query, min_results", CASES)
def test_search_returns_results(vibe, query, min_results):
    vibe.go("https://shop.example.com")
    vibe.find(role="searchbox").type(query)
    vibe.find(role="button", text="Search").click()
 
    assert vibe.find(".result").count() >= min_results

pytest reports test_search_returns_results[laptop-12], [phone-30], and [headphones-18] as three independent results — one red row tells you exactly which input broke. Combine parametrize with markers (@pytest.mark.smoke) to slice the suite: pytest -m smoke runs just the smoke cases. This composes cleanly with the context-per-test fixture above, so parametrized cases stay isolated from one another.

Which fixture scope should the browser use?

Match the scope to the trade-off you care about: launch speed versus isolation. pytest fixtures accept a scope (function, class, module, session), and the right choice for a browser suite is almost always session-scoped browser, function-scoped context. Launching Chrome is the expensive step; opening a context is cheap. Doing both once per test wastes seconds on every case.

Scope patternLaunch costIsolationUse when
Browser per test (function)High — new Chrome each testTotalTiny suites, or a test mutates browser-level state
Browser per session + context per testLow — Chrome starts onceTotal (per-context)Recommended default for most suites
Shared page, no contextLowestNone — state leaksNever for real tests; debugging only

The recommended pattern is the two-fixture split shown earlier: a scope="session" bro fixture that launches Chrome once, and a default (function-scoped) vibe fixture that opens and closes a context per test. You pay the browser startup once and still get a clean slate every test. Only fall back to a browser-per-test fixture when a test genuinely needs a pristine browser process, which is rare once you use contexts.

How do I combine page objects with pytest fixtures?

Return a page object from a fixture so tests speak in domain terms, not selectors. As a suite grows, repeating vibe.find(label="Email").type(...) across files becomes a maintenance liability — one markup change means editing many tests. A page object wraps those selectors in one place, and a fixture hands the test a ready instance built on the live vibe page.

# pages/login_page.py
class LoginPage:
    def __init__(self, vibe):
        self.vibe = vibe
 
    def open(self):
        self.vibe.go("https://app.example.com/login")
        return self
 
    def login(self, email, password):
        self.vibe.find(label="Email").type(email)
        self.vibe.find(label="Password").type(password)
        self.vibe.find(role="button", text="Sign in").click()
        return self
 
    def heading(self):
        return self.vibe.find("h1").text()
# conftest.py
import pytest
from pages.login_page import LoginPage
 
 
@pytest.fixture
def login_page(vibe):
    return LoginPage(vibe)

Now the test is short, readable, and immune to selector churn:

def test_login(login_page):
    login_page.open().login("user@example.com", "secret123")
    assert login_page.heading() == "Dashboard"

The fixture depends on vibe, so pytest still launches the browser and isolates the context for you — the page object just rides on top. The full pattern, including when not to reach for page objects, is in the Page Object Model in Vibium.

Can I run Vibium pytest tests in parallel?

Yes — install pytest-xdist and run pytest -n auto to spread tests across CPU cores. Parallelism is safe here precisely because each test already gets its own browser context, so there is no shared cookie jar or storage to collide.

pip install pytest-xdist
pytest -n auto            # one worker per CPU core

For real speed, launch one browser per worker rather than per test — a session-scoped bro fixture (as shown earlier) does exactly that, since xdist gives each worker its own session. Each test still spins up a fresh, cheap context off that shared browser. The trade-offs and worker patterns are covered in parallelize tests. If you run into ordering-dependent failures under -n, that is hidden state leakage — the fix is always tighter per-test isolation, not serial execution.

Prefer JavaScript? The same pattern in a test runner

The Vibium concepts map one-to-one if your team is on JS/TS — you just swap pytest for your JS runner and use Vibium's sync JS client. The fixture idea becomes a beforeEach/afterEach pair.

const { browser } = require('vibium/sync')
 
let bro, page
 
beforeEach(() => {
  const bro2 = browser.launch({ headless: process.env.HEADLESS === 'true' })
  bro = bro2
  page = bro2.page()
})
 
afterEach(() => {
  bro.close()
})
 
test('homepage title', () => {
  page.go('https://example.com')
  expect(page.find('h1').text()).toBe('Example Domain')
})

The mental model is identical: set up the browser before each test, drive it with find() and click(), assert on text(), tear it down after. Only the runner's syntax differs.

Vibium + pytest vs. plugin-based runners

Because Vibium leans on plain pytest fixtures instead of a bundled runner, the ergonomics differ from Playwright's pytest-playwright or Selenium setups. Here is an honest comparison.

AspectVibium + pytestPlaywright + pytest-playwrightSelenium + pytest
Required pluginNone — you write fixturespytest-playwright provides pageNone, but most add webdriver-manager
Browser/driver setupAuto-downloaded Chrome, one Go binaryplaywright install downloads browsersMatch driver to browser version manually
Fixture ownershipYou own the vibe fixture in conftest.pyPlugin owns page/browser fixturesYou own the driver fixture
Auto-waitingBuilt into find()Built into locatorsManual WebDriverWait / expected conditions
AI-native checkspage.check("cart is empty") availableNot availableNot available
Parallel testspytest-xdist + context isolationpytest-xdist, browser-per-workerpytest-xdist, care with shared driver

When to choose Vibium + pytest: you want auto-waiting and one-binary setup without a driver-matching dance, you like owning your fixtures explicitly rather than inheriting a plugin's, or you want the option of AI-native assertions like page.check() alongside deterministic ones. It is also a clean fit when moving off Selenium, where the driver/browser version mismatch is a constant CI headache — see Vibium vs Selenium.

When another tool may fit better today: if your team already standardizes on pytest-playwright and wants its bundled page fixture, auto-tracing, and mature reporting out of the box, that convenience is real — Vibium asks you to write a few lines of fixture yourself. Playwright is also more battle-tested across Firefox and WebKit right now; Vibium targets Chrome via BiDi. Weigh it fairly in Vibium vs Playwright.

Migration note / gotchas. Coming from pytest-playwright, the page fixture becomes a vibe fixture you define, and expect(locator).to_have_text(...) becomes a plain assert vibe.find(...).text() == .... Coming from Selenium, delete every WebDriverWait/time.sleep() — Vibium auto-waits, and leftover sleeps reintroduce the very flakiness you are trying to remove. In both cases, keep one browser context per test to preserve isolation the plugin used to give you.

Keep fixtures in conftest.py, group tests by feature, and read config from the environment. A predictable layout makes the suite navigable as it grows from a handful of tests to hundreds.

my-app-tests/
├── conftest.py          # vibe / bro fixtures + screenshot hook
├── pytest.ini           # markers, default options
├── tests/
│   ├── test_login.py
│   ├── test_search.py
│   └── test_checkout.py
└── pages/               # optional: page objects
    └── login_page.py

Register your markers in pytest.ini so pytest -m smoke works and --strict-markers catches typos:

[pytest]
markers =
    smoke: fast critical-path checks
    slow: long-running end-to-end flows
addopts = -v --strict-markers

As the suite scales, wrap repeated selector logic in page objects so a UI change is a one-file edit — see the Page Object Model in Vibium. For the broader suite-layout conventions this fits into, read structure a Vibium test suite, and for running it on every push, Vibium in CI/CD with GitHub Actions.

Common errors and how to fix them

Most Vibium-with-pytest friction traces back to a handful of setup mistakes, and each has a one-line fix. Knowing them saves a first-run debugging detour.

  • ModuleNotFoundError: No module named 'vibium' — the virtual environment is not active or Vibium is not installed there. Activate .venv and run pip install vibium. Confirm with pip show vibium.
  • Fixture not found: vibe — the fixture lives in the wrong place. It must be in conftest.py (not a random helper module) so pytest auto-discovers it, and conftest.py must sit at or above the test file in the directory tree.
  • rep_call missing / no screenshot on failure — the pytest_runtest_makereport hook is not registered. It has to be in conftest.py, and you should read it defensively with getattr(request.node, "rep_call", None) as shown, since setup-phase failures never reach the call phase.
  • Browser stays open after a crash — teardown did not run because setup raised. Keep browser.launch() as the first line after yield-free setup, and let pytest's fixture finalization call quit(); avoid doing risky work before the browser is assigned.
  • Tests pass alone but fail together — hidden state leakage. You are reusing one context or page across tests. Switch to bro.new_context() per test so cookies and storage reset every time.
  • Chrome fails to start on a Linux runner — missing shared libraries. Install libnss3, libgbm1, libasound2, and friends before running pytest, as detailed in the CI/CD guide.

If a test hangs instead of failing, it is usually a find() waiting out its actionability timeout on an element that never appears — the message names the selector, which points straight at the fix.

The Vibium + pytest checklist

  • Define one vibe (or bro + context) fixture in conftest.py; never launch the browser inside a test body.
  • Read headless from an environment variable so one fixture serves local and CI runs.
  • Assert with plain assert on text(), value(), is_visible(), count() — let pytest rewrite the failure output.
  • Give each test a fresh bro.new_context() for isolation; seed auth via cookies to skip slow UI logins.
  • Add the pytest_runtest_makereport hook and screenshot on failure for instant CI debugging.
  • Use @pytest.mark.parametrize for data-driven cases and markers to slice the suite.
  • Trust auto-waiting — delete every sleep() and WebDriverWait carried over from other tools.
  • Scale out with pytest-xdist (-n auto); context isolation keeps parallel runs safe.

Vibium supplies a clean, auto-waiting browser API; pytest supplies fixtures, assertions, and parallelism. Wire them together with a single fixture and you get a suite that is fast to write, stable to run, and trivial to debug.

Next steps

Frequently asked questions

How do I use Vibium with pytest?

Install both with pip install vibium pytest, then wrap the browser in a pytest fixture that launches Vibium, yields a page to the test, and quits afterward. Your test functions receive the page as an argument, call find() and click(), and assert on text() or is_visible().

Do I need a plugin like pytest-playwright to use Vibium?

No. Vibium works with plain pytest fixtures you write yourself in conftest.py. There is no required plugin. You define a browser fixture, control headless mode with an environment variable, and use standard pytest features like parametrize, markers, and fixtures for the rest.

How do I take a screenshot when a Vibium pytest test fails?

Add a pytest hook that records each test's outcome, then in the browser fixture check request.node.rep_call.failed after yield. If the test failed, call page.screenshot(full_page=True), write the PNG bytes to disk, and pytest or your CI job uploads it as an artifact.

How do I isolate state between Vibium pytest tests?

Give each test a fresh browser context with bro.new_context(). A context is an isolated cookie jar and storage sandbox, so logins, cookies, and local storage never leak between tests. Create the context in a function-scoped fixture and close it after the test.

How do I run Vibium pytest tests headless in CI?

Read headless mode from an environment variable in your fixture, for example headless = os.getenv('HEADLESS') == 'true'. Debug headed locally and set HEADLESS=true in CI. The same test code runs both ways, so there is no separate CI path to drift out of sync.

Can I run Vibium pytest tests in parallel?

Yes. Install pytest-xdist and run pytest -n auto. Because each test gets its own browser context, tests do not share cookies or storage and are safe to run concurrently. Keep the browser fixture function-scoped or use one browser per worker for speed.

Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.

Related guides