How to Use PyUnit to Test a Selenium Python Suite
Use Python’s built-in unittest framework to organize Selenium browser tests, manage WebDriver cleanup, run tests and suites, and diagnose common failures.
Use Python’s standard-library unittest module with Selenium WebDriver: subclass unittest.TestCase, create the browser in setUp, register driver.quit with addCleanup, and write methods whose names begin with test. “PyUnit” is a familiar name for this framework, not a separate package to install.
This guide uses Selenium’s Python bindings. The current Selenium documentation lists Python 3.10+ support; check its installation and support documentation for current requirements. In modern Selenium, Selenium Manager handles browser driver setup for most supported installations when you create a WebDriver. A separate driver download is not a universal prerequisite.
1. Install Selenium and create a project
Use a virtual environment so the Selenium dependency is isolated from other Python projects. These commands work in a POSIX shell:
mkdir selenium-unittest
cd selenium-unittest
python -m venv .venv
. .venv/bin/activate
python -m pip install selenium
On Windows PowerShell, activate the environment with .venv\Scripts\Activate.ps1. If activation is restricted, use the Python executable inside the environment directly, for example .venv\Scripts\python.exe -m pip install selenium.
Install a supported browser such as Chrome, Firefox, or Edge. Selenium Manager can resolve the matching driver in common setups. For managed CI images, restricted networks, pinned browser versions, or other special environments, you may need to provide browser and driver installation explicitly. See the Selenium documentation for current browser support and setup details.
2. Write a minimal Selenium unittest
Create test_selenium.py:
import unittest
from selenium import webdriver
class SeleniumTestCase(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.addCleanup(self.driver.quit)
def test_page_title(self):
self.driver.get("https://selenium.dev")
self.assertIn("Selenium", self.driver.title)
if __name__ == "__main__":
unittest.main()
Run it from the project directory:
python -m unittest test_selenium.py
The test case has three key parts:
unittest.TestCasegives the class test fixtures and assertion methods.setUpruns before each test method. The method name must start withtestfor unittest discovery.addCleanup(self.driver.quit)registers browser-session cleanup even if the test fails. Usequit()to end the session;close()closes only the current window and can leave the WebDriver session running.
Assertions describe the expected behavior and give the runner a useful failure result. Common choices include assertEqual, assertIn, assertTrue, assertFalse, and assertRaises. Prefer checking a visible outcome or page state relevant to the user journey over merely checking that navigation did not raise an exception.
3. Use setup and cleanup safely
Register cleanup immediately after creating each resource that needs cleanup. If driver creation itself fails, there is no session to quit; once it succeeds, addCleanup ensures the session is closed after the test. This is safer than putting quit() only at the end of a test method, because an assertion or exception could skip that line.
Use setUp and tearDown when a fixture has a simple, symmetric lifecycle. For WebDriver, cleanup registration is concise and remains associated with the test case:
def setUp(self):
self.driver = webdriver.Chrome()
self.addCleanup(self.driver.quit)
Each test method gets its own setup and cleanup cycle. This makes tests easier to run independently, but starting a browser per method adds time. Keep tests independent: do not rely on another test having run first or on test order. If you need a different lifecycle for a costly shared resource, make that design explicit and preserve isolation of browser state.
4. Run one test, a file, or a discovered suite
The unittest command accepts file paths, dotted module names, classes, and individual test methods. Examples:
# Run a file
python -m unittest tests/test_search.py
# Run a module (when tests is importable)
python -m unittest tests.test_search
# Run one test class
python -m unittest tests.test_search.SearchTests
# Run one method
python -m unittest tests.test_search.SearchTests.test_search_results
For a growing project, organize test modules in a directory and use discovery. By default, unittest looks for files named test*.py beneath the start directory:
python -m unittest discover
python -m unittest discover -s tests
python -m unittest discover -s tests -p "test_*.py"
A simple layout could be:
project/
tests/
test_homepage.py
test_search.py
.venv/
For explicit suite composition, define a TestSuite and add cases. Most projects can begin with discovery and add an explicit suite only when they need a particular grouping or execution order.
import unittest
from tests.test_search import SearchTests
from tests.test_homepage import HomepageTests
suite = unittest.TestSuite()
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(HomepageTests))
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(SearchTests))
if __name__ == "__main__":
unittest.TextTestRunner(verbosity=2).run(suite)
The standard library documents test cases, fixtures, suites, runners, and command-line execution in the unittest reference.
5. Wait for page state instead of sleeping blindly
Page navigation returning does not always mean that the specific element your test needs is ready. Use an explicit wait for a condition rather than a fixed sleep where possible. This example waits for a search result to become visible:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def test_search_result_appears(self):
self.driver.get("https://example.com/search")
result = WebDriverWait(self.driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-test='result']"))
)
self.assertTrue(result.is_displayed())
Replace the example URL and selector with ones from the application under test. An explicit wait has a bounded timeout and waits for the condition; a hard-coded delay always consumes the full delay and can still be too short on a slow run. Avoid mixing implicit and explicit waits without understanding the timing interaction, as it can make wait duration difficult to predict.
6. Choose browser options when the environment requires them
For headless CI execution or custom browser arguments, configure browser options before constructing the driver. For example, Chrome options can be supplied to webdriver.Chrome:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
Keep options tied to a real environment requirement. Headless mode can behave differently from a visible desktop session, and browser flags vary across browser versions. For a local debugging run, omit headless mode so you can see the browser. Consult the browser-specific Selenium documentation for supported options.
7. Run unittest tests with pytest (optional)
You can keep unittest test cases and run many of them with pytest. Install pytest into the same environment:
python -m pip install pytest
python -m pytest
By default, pytest collects TestCase subclasses and test methods from files matching test_*.py and *_test.py. It can add selection controls, output capture, plugins, and different reporting while preserving most unittest tests. Compatibility is not total: check pytest’s current unittest compatibility guide if your suite uses special unittest protocols such as load_tests.
| Need | Start with |
|---|---|
| No extra test-runner dependency | unittest, included with Python |
| Existing unittest tests plus pytest selection or plugins | pytest, after checking compatibility for features your suite uses |
| Remote browser sessions | Selenium Remote WebDriver or Grid, when remote execution is needed |
Selenium’s test organization guide discusses runner choices. Neither runner is a universal fit: use the built-in runner for a small dependency footprint, and consider pytest when its selection, reporting, or plugin ecosystem serves your project.
8. Use Remote WebDriver when local execution is not enough
A local Selenium script does not require a Selenium server. Remote WebDriver and Grid are optional when browser sessions need to run on another machine or in a managed browser environment. A remote driver connects to a configured command endpoint; the endpoint, authentication, browser capabilities, and network access depend on the Grid or service you operate.
Keep the same unittest lifecycle pattern and ensure the remote session is quit in cleanup. Remote execution adds network and infrastructure failure modes, so distinguish a test assertion failure from a browser allocation, endpoint, or connection failure in your logs. See Selenium’s official documentation for Remote WebDriver setup.
9. Capture a visual artifact for debugging
When a UI test fails, a screenshot can help show the browser state at the point of failure. Save one in the failure path, but preserve the original exception and always close the browser:
import os
import unittest
from selenium import webdriver
class ScreenshotOnFailure(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.addCleanup(self.driver.quit)
def tearDown(self):
if getattr(self, "_outcome", None) and self._outcome.result.errors:
os.makedirs("artifacts", exist_ok=True)
self.driver.save_screenshot("artifacts/failure.png")
Internal unittest result details can vary between Python versions, so for a larger suite prefer a runner or framework hook designed for failure artifacts rather than depending on private internals. Another straightforward option is to save screenshots explicitly around a known fragile step or from a helper that catches an exception, records the screenshot, and re-raises it.
10. Troubleshooting common Selenium unittest failures
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'selenium' |
Selenium was installed into a different Python environment. | Activate the project virtual environment and run python -m pip install selenium with the same python used to run tests. |
| Driver or browser cannot be found | Browser is missing, installation is unsupported, or Selenium Manager cannot obtain a driver in this environment. | Install a supported browser; check network and permissions; inspect Selenium Manager output. In restricted or pinned environments, configure the browser and driver explicitly. |
SessionNotCreatedException |
Browser and driver versions or capabilities are incompatible, or the browser cannot start in the environment. | Update Selenium and the browser setup, remove inappropriate options, and verify the browser can launch in that environment. |
| Test file runs zero tests | Test method does not start with test, class does not subclass TestCase, or discovery pattern does not match the filename. |
Use a TestCase subclass, name methods test_..., and use a test*.py filename or pass an explicit path. |
| Element lookup fails intermittently | Test queried before the page reached the expected state, or selector is unstable. | Wait for a specific visible/clickable condition and prefer stable application-owned selectors such as test attributes. |
| Browser remains running after a failure | Cleanup was placed after an assertion or only used close(). |
Register driver.quit with addCleanup immediately after driver creation. |
| Tests pass alone but fail in a suite | Tests share state, depend on order, or reuse mutable browser data. | Make each test establish and clean up its own state; inspect external data and session assumptions. |
| pytest finds no tests | Filename or method does not match default collection patterns. | Use test_*.py or *_test.py and test method names beginning with test, or configure collection deliberately. |
11. Performance, reliability, and cost
Browser startup and page loading are usually the expensive parts of a Selenium test, rather than unittest’s assertion machinery. Keep the suite reliable by waiting for meaningful conditions, keeping tests independent, avoiding unnecessary navigation, and collecting enough failure context to diagnose flakiness. Run a focused test while iterating, then use discovery or the project’s CI runner for the full suite.
Local Selenium itself does not require a hosted browser service; the costs are the machine time and any browser infrastructure you choose to operate. Remote execution can move browser management elsewhere but introduces endpoint availability, network latency, and service or infrastructure costs. Do not treat retries as a fix for nondeterministic tests: a retry can conceal a timing or state problem.
Or skip the browser setup
If you need a page image rather than browser interaction and assertions, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; its options include full-page capture, element capture, viewport and device settings, waits, custom CSS or JavaScript, and more. The API documentation has the request parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://selenium.dev -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://selenium.dev"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://selenium.dev'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(async fs => {
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
});
- Cookie banners are accepted before capture, and 60+ known consent platforms, newsletter popups, and chat widgets can be removed before the shot; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Is PyUnit a separate package from unittest?
No. In this context, PyUnit refers to Python’s unittest framework. Import and use unittest; there is no additional PyUnit package required for this pattern.
Can I use Selenium with unittest without pytest?
Yes. Python includes unittest, and Selenium’s Python bindings can be used directly from its test cases.
Do I need Selenium Grid for my first test?
No. A local browser is enough for a basic test. Grid or Remote WebDriver is an optional route for remote browser execution.
Can one unittest suite target more than one browser?
Yes. Make browser choice configurable in setup or create separate test configurations. Keep browser-specific capabilities and environment setup explicit so failures identify which configuration ran.


