Everything you need to run automated browser tests with pytest and Selenium, from your first test to parallel cross-browser execution.

Salman Khan
April 6, 2026
On This Page
Pytest is a popular Python testing framework that is mainly used for unit testing. It is an open-source framework with more than 8.5k Stars and 2k Forks on the pytest GitHub repository. Pytest provides an effective way to write and execute scalable test cases or test suites and generate extensive test reports. Selenium automation testing with pytest helps you write scalable tests for different testing types like cross browser testing, database testing, Pytest API testing, and more.
If you're looking for a complete Selenium pytest tutorial, you've come to the right place. Here we cover everything related to the Pytest framework - right from Pytest basics to Selenium testing with pytest, and explore advanced use cases of Selenium pytest with detailed examples.
Let's begin!
Overview
pytest is a Python testing framework that handles test discovery, setup, teardown, and reporting. Selenium controls the browser. Together they give you a clean, scalable foundation for automated browser testing without the boilerplate that unittest requires.
Hard sleeps, brittle locators, no shared setup code, and tests that depend on each other are the most common culprits. This guide shows you how to avoid all of them from day one.
This guide covers everything you need to write and scale a Selenium pytest suite, including:
pytest is a Python testing framework built for simplicity and scale. It is open-source, has over 8.5k stars on the pytest GitHub repository, and is actively maintained.
It works for everything from simple unit tests to complex browser automation suites, and does all of this with significantly less boilerplate than Python's built-in unittest module.
A few things that make pytest stand out for selenium pytest testing:
Auto-discovery is built in. The pytest framework scans your project for files named test_*.py or *test.py and runs any function whose name starts with test. No test registration or class inheritance required.
Failure output is readable. When a pytest selenium test fails, pytest shows you the exact line, the values that were compared, and the assertion that failed.
Fixtures handle setup and teardown cleanly. Instead of repetitive setUp and tearDown methods, you define fixture functions once and inject them into any test that needs them.
Plugins extend everything. pytest-selenium simplifies Selenium setup. pytest-xdist adds parallel execution. pytest-html generates shareable HTML reports.
Selenium pytest helps you write scalable tests for different testing types including cross-browser testing, database testing, and pytest API testing.
For a detailed look at the basics of the pytest framework, its advantages and disadvantages, and how to perform Selenium Python testing, see the guide on basics of the pytest framework.
Before writing any tests, install the required packages. Create a virtual environment first to keep your project dependencies isolated:
python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate
Now install Selenium, the pytest framework, and webdriver-manager:
pip install selenium pytest webdriver-manager
webdriver-manager handles browser driver downloads automatically so you do not need to manually download ChromeDriver or GeckoDriver. Verify everything is installed:
pytest --version
You can also install the pytest-selenium plugin which provides a built-in Selenium fixture and simplifies driver configuration:
pip install pytest-selenium
With pytest-selenium, you can pass the browser directly on the command line without changing your test code:
pytest --driver Chrome pytest --driver Firefox
For a detailed walkthrough of the full installation and environment setup, see our guide on setting up pytest with Selenium WebDriver.
Here is a simple selenium pytest test that opens a browser, navigates to a URL, and verifies the page title:
# test_first.py
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
def test_page_title():
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get("https://www.testmuai.com")
assert "TestMu AI" in driver.title
driver.quit()Run it from your terminal:
pytest test_first.py -v
The -v flag gives verbose output showing each test name and its pass or fail status. This works, but calling driver.quit() inside every test is not scalable. Fixtures solve this.
For a deeper look at how Selenium WebDriver with Python works, including locator strategies and explicit waits, check out our dedicated guide.
pytest fixtures are functions that run before and after your test functions, handling setup and teardown automatically. For selenium pytest testing, a fixture that creates and destroys the WebDriver instance is the standard approach. Learn more in our end-to-end tutorial for pytest fixtures.
Create a conftest.py file in your project root. The pytest framework loads this file automatically and makes its fixtures available to every test file without any imports:
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
@pytest.fixture(scope="function")
def driver():
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.maximize_window()
yield driver
driver.quit()The yield statement is the key. Everything before it is setup. Everything after it is teardown. The scope="function" parameter means a fresh browser instance is created and destroyed for each test function. Change scope to "module" or "session" if you want to share a single browser across multiple tests.
Now your selenium pytest tests are clean and focused on what they actually test:
# test_example.py
def test_page_title(driver):
driver.get("https://www.testmuai.com")
assert "TestMu AI" in driver.title
def test_navigation(driver):
driver.get("https://www.testmuai.com")
driver.find_element("link text", "Pricing").click()
assert "pricing" in driver.current_url.lower()Pass the fixture name as a parameter and pytest injects it automatically. This is dependency injection, and it is one of the most powerful features the pytest framework brings to automation testing.
Before writing complex selenium pytest tests, you need to understand how Selenium locates elements. The By class provides several locator strategies:
from selenium.webdriver.common.by import By # By ID - fastest and most reliable when available element = driver.find_element(By.ID, "username") # By CSS selector - flexible and widely used element = driver.find_element(By.CSS_SELECTOR, ".login-btn") # By XPath - powerful but brittle when overused element = driver.find_element(By.XPATH, "//button[@type='submit']") # By link text element = driver.find_element(By.LINK_TEXT, "Sign In")
ID is the most reliable locator when available. CSS selectors are the next best option. XPath is powerful for complex queries but breaks easily if the page structure changes.
For elements that load dynamically, always use explicit waits instead of time.sleep(). Hard sleeps are one of the most common causes of flaky pytest selenium tests:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.ID, "login-btn"))) element.click()
WebDriverWait polls the DOM at short intervals until the condition is met or the timeout expires. If the element appears in 300ms, your test continues in 300ms.
Parameterization in pytest lets you run the same selenium pytest test with multiple sets of inputs without duplicating code. Use the @pytest.mark.parametrize decorator:
import pytest
from selenium.webdriver.common.by import By
@pytest.mark.parametrize("username,password,expected", [
("valid_user", "valid_pass", "dashboard"),
("invalid_user", "wrong_pass", "login"),
("", "", "login"),
])
def test_login(driver, username, password, expected):
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys(username)
driver.find_element(By.ID, "password").send_keys(password)
driver.find_element(By.ID, "login-btn").click()
assert expected in driver.current_urlThis runs three separate test cases from a single function. Each combination appears as its own test in the output. If one fails, the others still run, and you get precise output telling you exactly which data set caused the failure.
You can also parametrize your WebDriver fixture to run every test across multiple browsers automatically:
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.firefox import GeckoDriverManager
@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
if request.param == "chrome":
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
else:
driver = webdriver.Firefox(service=Service(GeckoDriverManager().install()))
yield driver
driver.quit()Every test that uses this fixture now runs on both Chrome and Firefox automatically. For a deeper dive, see our guide on parameterization in pytest with Selenium.
Running pytest selenium tests sequentially becomes a problem as your suite grows. A suite of 200 tests running one after the other can take 30 minutes or more. The pytest-xdist plugin enables parallel test execution:
pip install pytest-xdist
Run tests in parallel with the -n flag:
pytest -n 4 # Run across 4 parallel workers pytest -n auto # Use all available CPU cores
For true parallel testing across different browsers and OS combinations, run your pytest selenium suite on TestMu AI's cloud Selenium Grid. See our guide on parallel testing with pytest and Selenium Grid for the full setup. Update your conftest.py:
import os
import pytest
from selenium import webdriver
@pytest.fixture(scope="function")
def driver():
username = os.environ.get("LT_USERNAME")
access_key = os.environ.get("LT_ACCESS_KEY")
capabilities = {
"browserName": "Chrome",
"browserVersion": "latest",
"platformName": "Windows 10",
"LT:Options": {
"build": "Pytest Selenium Build",
"name": "Parallel Test"
}
}
driver = webdriver.Remote(
command_executor=f"https://{username}:{access_key}@hub.lambdatest.com/wd/hub",
desired_capabilities=capabilities
)
yield driver
driver.quit()This connects your pytest framework to TestMu AI's automation cloud, giving you access to hundreds of browser and OS combinations without managing any local infrastructure.
The pytest framework discovers and runs all functions starting with test_ in any matching file. You can have as many test functions in one file as makes sense for your suite:
# test_homepage.py
from selenium.webdriver.common.by import By
def test_title(driver):
driver.get("https://www.testmuai.com")
assert "TestMu" in driver.title
def test_logo_visible(driver):
driver.get("https://www.testmuai.com")
logo = driver.find_element(By.CSS_SELECTOR, ".logo")
assert logo.is_displayed()
def test_cta_button(driver):
driver.get("https://www.testmuai.com")
cta = driver.find_element(By.LINK_TEXT, "Get Started Free")
assert cta.is_displayed()To run only a subset of selenium pytest tests, use the -k flag:
pytest test_homepage.py -k "title or logo"
For larger suites, group tests with markers. Register them in pytest.ini:
[pytest]
markers =
smoke: fast tests for basic functionality
regression: full regression suiteApply markers to test functions and run selectively:
@pytest.mark.smoke
def test_title(driver):
...
pytest -m smoke
pytest -m regressionFor more scenarios and examples, see our guide on executing multiple test cases from a single pytest file.
When running a large pytest selenium suite in CI/CD, you may not want to wait for everything to complete after a critical failure. Use the --maxfail flag:
pytest --maxfail=3 pytest -x
The -x shorthand is the most common choice during local development. Fix one failure at a time rather than seeing a wall of red at the end of a 30-minute run.
For a detailed look at all the options, see our guide on how to stop a test suite after N test failures in pytest.
The pytest framework produces plain terminal output by default. For reports you can share with the team or attach to CI/CD builds, install pytest-html:
pip install pytest-html
Generate an HTML report:
pytest --html=report.html --self-contained-html
The --self-contained-html flag bundles all CSS and JavaScript so the file opens anywhere without dependencies.
To automatically capture a screenshot on selenium pytest test failure and attach it to the report, add this hook to your conftest.py:
from pytest_html import extras
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver:
screenshot = driver.get_screenshot_as_base64()
report.extras = [extras.image(screenshot)]Now every failing test includes a browser screenshot at the point of failure.
For XML reports compatible with Jenkins and other CI tools:
pytest --junitxml=results.xml
For the full breakdown of report formats and customization options, see our guide on pytest report generation for Selenium automation scripts.
Use conftest.py for shared fixtures. Keep your WebDriver setup, teardown, and shared test data there so all test files can access them without importing anything manually.
Keep selenium pytest tests independent. Each test should set up its own state and not rely on another test having run first. Tests that share state fail in unpredictable order and are the hardest bugs to track down.
Use explicit waits, never time.sleep(). Hard sleeps make pytest selenium tests slow and produce false failures when pages load slower than expected.
Follow the Page Object Model for large suites. Separate your locators and page interactions into dedicated page classes. When a locator changes, you update it once, not in every test that uses it.
Use pytest-selenium for simpler driver configuration. The plugin handles setup and teardown for you and lets you switch browsers from the command line without touching your test code.
Run on real browsers and OS combinations. Testing only on your local Chrome gives you false confidence. Run your suite on TestMu AI's Selenium Grid across multiple browsers and OS combinations to catch compatibility issues before your users do.
Integrate with CI/CD. Tests that only run locally do not protect production. Connect your pytest framework to your pipeline so tests run automatically on every push or pull request.
pytest is the most widely used Python framework for Selenium automation because it requires no boilerplate, auto-discovers tests, and supports fixtures and plugins out of the box.
A conftest.py fixture with yield handles WebDriver setup and teardown automatically, giving every test a clean browser session without repetitive code.
WebDriverWait should replace time.sleep() in every Selenium test. It exits as soon as the condition is met instead of waiting for a fixed duration.
The @pytest.mark.parametrize decorator runs one test function across multiple data sets and browsers, eliminating code duplication and increasing coverage.
pytest-xdist enables parallel test execution with a single -n flag, cutting total suite runtime significantly on large test sets.
A maintainable Selenium pytest suite keeps tests independent, uses the Page Object Model for locators, and runs on real browsers to catch issues before users do.
Did you find this page helpful?
More Related Hubs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance