How to Parameterize Selenium Tests with pytest
Use pytest parameters for test data and fixtures for browser setup. Learn how to add readable case IDs, manage teardown, and debug focused Selenium runs.
Use @pytest.mark.parametrize when a test should run with several inputs, such as URLs and expected titles. Put WebDriver creation and cleanup in a fixture. When the browser choice itself varies, parameterize the fixture; use indirect=True when test data should configure that fixture. Give cases explicit IDs so failures are easy to identify and rerun.
The usual pattern is one browser fixture shared by each test invocation, with pytest creating and tearing down the browser for that invocation:
import pytest
from selenium import webdriver
@pytest.fixture
def driver():
browser = webdriver.Chrome()
try:
yield browser
finally:
browser.quit()
@pytest.mark.parametrize(
"url, expected_title",
[
pytest.param("https://example.com/", "Example Domain", id="example"),
pytest.param("https://www.selenium.dev/", "Selenium", id="selenium-home"),
],
)
def test_page_title(driver, url, expected_title):
driver.get(url)
assert expected_title in driver.title
The two URLs and title expectations are illustrative examples, not claims that these pages have been tested. The key separation is that pytest supplies each input pair while the fixture owns the browser lifecycle. See pytest’s parametrization guide and the Selenium Python API example.
1. Choose where the parameters belong
| What varies | Use | Example |
|---|---|---|
| Inputs to one test | @pytest.mark.parametrize |
URL, expected text, form data, feature flag |
| Resource configuration used by dependent tests | @pytest.fixture(params=...) |
Chrome versus Firefox WebDriver |
| A test value that should configure an expensive fixture | indirect=True |
Browser name passed to a driver fixture |
| Cases derived from command-line options or collection rules | pytest_generate_tests |
Environment-specific generated cases |
Start with direct test parameters for a short, fixed input list. Move variation to a fixture when it changes how a resource is built, or when multiple tests should run against each fixture configuration. Use a generation hook only when the case list genuinely needs to be created dynamically.
2. Parameterize browser fixtures
A fixture parameter makes pytest set up a browser for each configured value and pass it to tests that request the fixture. The following fixture demonstrates local Chrome and Firefox setup:
import pytest
from selenium import webdriver
@pytest.fixture(params=["chrome", "firefox"], ids=["chrome", "firefox"])
def driver(request):
if request.param == "chrome":
browser = webdriver.Chrome()
elif request.param == "firefox":
browser = webdriver.Firefox()
else:
raise ValueError(f"Unsupported browser: {request.param}")
try:
yield browser
finally:
browser.quit()
def test_example_page_loads(driver):
driver.get("https://example.com/")
assert "Example Domain" in driver.title
This is a pattern example; adapt browser options and runtime setup to your project. Selenium Manager handles browser and driver setup for many supported local environments in current Selenium releases. Check the documentation for the Selenium version you install, especially for less common platforms or when you need explicit driver installation. Local scripts do not require Selenium Server; remote WebDriver uses Selenium Grid. See Selenium WebDriver getting started.
3. Use indirect parametrization to configure setup
With direct parametrization, pytest passes the value to the test function. With indirect=True, pytest passes the value into the named fixture as request.param. This is useful when browser construction is expensive and should happen during test execution rather than while collecting test cases.
import pytest
from selenium import webdriver
@pytest.fixture
def driver(request):
browser_name = request.param
if browser_name == "chrome":
browser = webdriver.Chrome()
elif browser_name == "firefox":
browser = webdriver.Firefox()
else:
raise ValueError(f"Unsupported browser: {browser_name}")
try:
yield browser
finally:
browser.quit()
@pytest.mark.parametrize(
"driver",
["chrome", "firefox"],
indirect=True,
ids=["chrome", "firefox"],
)
def test_homepage_title(driver):
driver.get("https://example.com/")
assert "Example Domain" in driver.title
You can also pass a list of fixture argument names to indirect when only some arguments should be routed through fixtures. Keep direct values direct unless they actually configure setup; that keeps test signatures straightforward.
4. Combine input and browser matrices deliberately
Stacking parametrization decorators forms combinations. For example, two URLs and two browsers produce four test cases. This is useful when every pair matters, but the number of browser launches grows as the product of the dimensions.
@pytest.mark.parametrize(
"url",
[
pytest.param("https://example.com/", id="example"),
pytest.param("https://www.selenium.dev/", id="selenium-home"),
],
)
def test_title_on_each_browser(driver, url):
driver.get(url)
assert driver.title
When driver is the parametrized browser fixture above, this test runs for each URL and each browser. For a smaller, intentional matrix, represent complete cases explicitly instead of generating every possible combination:
@pytest.mark.parametrize(
"browser_name, url",
[
pytest.param("chrome", "https://example.com/", id="chrome-example"),
pytest.param("firefox", "https://www.selenium.dev/", id="firefox-selenium-home"),
],
)
def test_selected_browser_url_pairs(browser_name, url):
...
For this last form, route browser_name through a fixture with indirect=["browser_name"], or use a browser fixture with a matching name. Choose cases based on needed behavior, isolation, launch cost, and ease of maintenance.
5. Make cases readable, mark exceptions, and rerun one case
Use ids= for a list of cases or pytest.param(..., id="...") when IDs belong beside individual values. A parameter can also carry marks, such as an expected failure:
@pytest.mark.parametrize(
"browser_name, expected",
[
pytest.param("chrome", "Example Domain", id="chrome-title"),
pytest.param(
"legacy-browser",
"Example Domain",
id="known-unsupported-browser",
marks=pytest.mark.xfail(reason="Browser is not supported in this environment"),
),
],
)
def test_title(browser_name, expected):
...
Use xfail only when the failure is understood and intentionally accepted; it should not conceal an unexplained environment problem. Pytest includes parameter IDs in collected node IDs. Inspect them with pytest --collect-only -q, then target a single case:
pytest tests/test_pages.py::test_page_title[selenium-home]
The exact node ID depends on the file, test name, and chosen ID.
6. Keep test data and browser state isolated
- Parameter objects are reused as passed. Pytest does not copy parameter values for each invocation. Avoid mutating a shared list or dictionary in a test. Build fresh mutable data inside a fixture or test when each case needs its own copy.
- Use a fresh browser when state matters. A new WebDriver per case helps prevent cookies, local storage, open tabs, and navigation state from leaking between cases. Reuse sessions only when the test design explicitly accounts for shared state and cleanup.
- Always close the session. Put
quit()afteryield, preferably infinally, so teardown runs after assertion failures too. - Do not add dimensions without a behavior reason. Combining every browser, locale, viewport, and input can multiply execution time and maintenance work.
See pytest’s notes on parameter values and combinations.
7. Run and diagnose the suite
- Install pytest and Selenium in the project environment, and ensure a supported browser is available.
- Run the whole suite with
pytest. - List collected parameter cases with
pytest --collect-only -q. - Rerun the exact failed case using its node ID, including the bracketed parameter ID.
- If running remotely, check Grid availability and the remote browser capabilities separately from test assertions.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
fixture 'driver' not found |
The fixture is outside the test’s discovery scope or has a different name. | Define it in the test module or a discovered conftest.py, and request the same fixture name. |
request.param is missing |
The fixture expects an indirect parameter, but the test parametrizes directly (or did not pass a value). | Set indirect=True for that fixture argument, or switch to a direct test argument. |
| Browser or driver cannot be found | Browser installation, permissions, platform support, or driver resolution is not suitable for the environment. | Check Selenium’s setup documentation and installed versions; configure the browser or driver explicitly if Selenium Manager cannot resolve it. |
| Remote session creation fails | Grid endpoint is unavailable or requested capabilities are unsupported. | Check Grid status, endpoint, and capabilities; reproduce with one browser case first. |
| One case changes another case’s result | Mutable test data or a reused browser session leaked state. | Stop mutating parameter objects, construct fresh data, or give each invocation its own browser. |
| Targeted rerun says no tests found | The node ID or parameter ID does not match collection output. | Copy the exact node ID from pytest --collect-only -q. |
| Suite is much slower after adding parameters | Browser launches or Cartesian combinations multiplied. | Remove unnecessary dimensions, use an explicit pair list, or split a broad matrix into focused tests. |
| Browser process remains after failure | Teardown was not reached or cleanup was not protected. | Use a yielding fixture with try/finally and call quit(). |
8. Performance, reliability, and cost
Parametrization adds test invocations; it does not make browser work cheaper by itself. A rough planning count is the number of explicit cases multiplied by each fixture parameter dimension in use. Browser startup, page load, and remote session allocation usually dominate the added work, but actual time depends on the environment and application, so measure your own suite rather than assuming a fixed cost.
Keep the matrix tied to distinct behavior, use case IDs to isolate failures, and prefer one browser per case when test isolation matters. If startup cost becomes significant, first identify redundant combinations and measure whether session reuse is safe for the application’s state. For reliability, make teardown unconditional, keep parameter data immutable, and validate the browser setup independently from page assertions.
Or skip the browser setup
If the task is to capture a page image or PDF rather than interact with it as a Selenium test, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF; see the API documentation for options and formats.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture; 60+ known consent platforms, newsletter popups, and chat widgets can be removed. Each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server exposes screenshot, page information, and PDF capture tools to AI agents.
- The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can I parameterize a test class or module?
Yes. Pytest supports parametrization at function, class, and module levels. Use the narrowest scope that clearly expresses the cases.
Should I use a browser fixture or pass a WebDriver as a test parameter?
Use a fixture for WebDriver lifecycle and setup. Pass browser choices or configuration values as parameters, then let the fixture create the resource.
When should I use pytest_generate_tests?
Use it when test cases need to be generated from collection-time rules, such as command-line options. For a fixed list, ordinary parametrization is simpler.


