HomebaseWorks
Lesson 4 of 4

Structuring tests with pytest: fixtures, assertions, screenshots and headless runs

Set up and tear down the browser with fixtures, write assertions that explain failures, save screenshots on failure and run headless in CI.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

Automation screenings rarely stop at one script. They ask how you would organise fifty tests, how you keep them independent, and how they run on a server with no screen. The answers come from the test framework, not from Selenium itself. This lesson uses pytest; TestNG and JUnit follow the same shape.

Advertisement

Fixtures for setup and teardown

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture
def driver():
    opts = Options()
    opts.add_argument("--headless=new")
    opts.add_argument("--window-size=1366,768")
    d = webdriver.Chrome(options=opts)
    d.implicitly_wait(0)
    yield d
    d.quit()

Each test receives a fresh browser and the code after yield closes it even when the test fails. Fresh state is what makes tests independent: test B must not rely on test A having logged in. If startup is slow, scope the fixture to a module and clear cookies between tests instead. Keep implicit waits at zero and use explicit waits in page objects, so the two never fight each other.

Assertions that explain the failure

# test_login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def test_valid_login_shows_dashboard(driver):
    driver.get("https://example.com/login")
    driver.find_element(By.ID, "username").send_keys("qa_user")
    driver.find_element(By.ID, "password").send_keys("Secret123")
    driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
    heading = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    assert heading.text == "Dashboard", f"Expected Dashboard, got {heading.text!r}"
    assert "/dashboard" in driver.current_url

One test, one behaviour, a name that reads as a sentence. Assert the thing the user cares about (the heading and URL), not that a click "worked". Add a message so the report says what was found. For several data rows, use @pytest.mark.parametrize rather than copying the test.

Screenshots on failure and running in CI

# in conftest.py
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    if rep.when == "call" and rep.failed:
        d = item.funcargs.get("driver")
        if d:
            d.save_screenshot(f"shots/{item.name}.png")

Run with pytest -v --junitxml=report.xml so the CI tool reads results, and add -n 4 with pytest-xdist to run in parallel once tests are independent. Headless mode is the default on servers; keep the window size fixed, because responsive layouts hide elements at small widths and cause failures that never happen on your laptop.

Common mistakes and a practice habit

  • Tests that pass only in a fixed order.
  • Assertions on element presence rather than on the outcome.
  • Hard-coded credentials and URLs in every file; put them in environment variables or a config.
  • Running headless with the default small window and chasing "element not visible" errors.

Practice habit. Convert three scripts you have into pytest tests with the fixture above. Run them in random order with pytest -p random_order or by shuffling manually, then headless, then in parallel. Each step that fails teaches a dependency you did not know was there.

Advertisement
Continue with Selenium4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Software testing