This episode covers building reusable test utilities and wrappers, custom logs, screenshots, and reporting utilities, integration with test frameworks like pytest, and sharing utilities across test suites so the test project stays concise.

The bigger the suite, the more often you find repeating patterns: creating drivers, waiting for elements, taking screenshots, writing logs. Episode 18 covers organizing those patterns into custom utilities and tooling — code written once and used across the whole suite.
The main goal of this episode isn't just less typing, but a suite that's easier to maintain and more informative. With consistent wrappers, policy changes in one place; with centralized logging and reporting, every failure carries complete evidence.
Centralize driver creation in one wrapper that handles options, headless, and session logging:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def buat_chrome(headless=False, ukuran=None):
opsi = Options()
if headless:
opsi.add_argument("--headless")
if ukuran:
opsi.add_argument(f"--window-size={ukuran[0]},{ukuran[1]}")
return webdriver.Chrome(options=opsi)buat_chrome(headless=False, ukuran=None) standardizes how the whole suite creates Chrome. An option change — for example adding an anti-crash argument — is done once.
Methods used by all pages can be pulled into a parent class:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def klik_saat_klikable(self, locator):
self.wait.until(EC.element_to_be_clickable(locator)).click()
def baca_teks(self, locator):
return self.driver.find_element(*locator).textklik_saat_klikable(locator) and baca_teks(locator) become methods inherited by every page object from episode 6. This consistency guarantees every page applies the same wait policy.
Logs scattered across many files are hard to trace. Centralize them via the logging module:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger("selenium-tests")
def cek_halaman(driver, url):
log.info("Membuka %s", url)
driver.get(url)
log.info("Judul: %s", driver.title)logging.getLogger("selenium-tests") gives one logger used by the whole suite. With log.info(...) at key points, the test's step order can be reconstructed from the log when a failure occurs.
The screenshots from episode 7 can be wrapped into a utility:
from pathlib import Path
def simpan_bukti(driver, nama, folder="artifacts"):
Path(folder).mkdir(exist_ok=True)
path_png = Path(folder) / f"{nama}.png"
driver.save_screenshot(str(path_png))
return path_pngsimpan_bukti(driver, nama) standardizes where and how evidence is named and stored. Used together with the pytest failure hook, it ensures every run produces consistent artifacts.
pytest integrates utilities through fixtures in conftest.py — this is where the driver wrapper and logger come together:
import pytest
import logging
from utils.driver_factory import buat_chrome
@pytest.fixture
def driver():
log = logging.getLogger("selenium-tests")
log.info("Membuat sesi Chrome")
d = buat_chrome(headless=True)
yield d
d.quit()
log.info("Sesi ditutup")This driver fixture provides a clean session to every test, complete with log entries for creation and closing. The whole suite uses the same fixture — setup and teardown quality is guaranteed in one place.
Add a hook to enrich reports with automatic evidence:
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
log = logging.getLogger("selenium-tests")
log.error("Test gagal: %s", item.name)
log.error("Detail: %s", report.longrepr)This hook writes the failure reason to the log at error level. The combination of the driver fixture, logger, and this hook produces a suite where every failure comes with context.
Good utilities are usable in other projects. The only way to achieve that is writing them without dependencies on specific pages or tests:
driver_factory.py — only about creating drivers.waits.py — only about waiting for conditions.reporting.py — only about storing evidence.utils/
├── driver_factory.py -> driver creation
├── waits.py -> wait conditions
├── reporting.py -> screenshots and logs
└── __init__.pyThe utils folder above never touches any page's business logic. A package like this can be published or copied to another project without major changes.
Tip
When creating utilities, write a short docstring up front and use clear names. Utilities without documentation quickly become "mystery code" that no one dares to change.
Episode 18 closes out the basic tooling needs of a Selenium suite: consistent driver wrappers and BasePage, centralized logging and screenshots, pytest fixtures and hooks that tie it all together, and a utils structure portable across projects.
Key takeaways:
In episode 19 next, we'll cover operational readiness and runbooks — runbooks for broken tests, maintenance windows, and environment drift, managing the test suite and triaging failures, recovery steps for browser compatibility issues, and test ownership and maintenance by the team.