Python Automation Testing: Frameworks and Best Practices
Choose Python test tools by scope: use unittest or pytest for code, and Selenium or Playwright for browser behavior. Includes runnable examples and CI guidance.
Choose a Python testing tool by what you need to verify. Use unittest when the standard library and class-based tests suit your project; use pytest for a flexible test runner with plain assertions and fixtures. Use Selenium or Playwright when you need to exercise a real browser. Browser automation tools complement test runners: Selenium can be used with either unittest or pytest, and Playwright’s Python CI guidance uses pytest.
There is no universal best framework. Start with test scope, existing code, browser coverage, team workflow, and what your CI environment can install and run.
1. Match the tool to the test
| Need | Good starting point | Why |
|---|---|---|
| Test functions and modules without adding a test-runner dependency | unittest |
It ships with Python and includes cases, fixtures, suites, runners, command-line execution, and test discovery. |
| Build a new general-purpose test suite with concise tests and reusable setup | pytest |
It supports plain assert statements with detailed failure introspection, automatic discovery, modular fixtures, and existing unittest tests. |
| Check a website through actual browser interactions | Selenium or Playwright, alongside a test runner | They automate browsers; they do not replace the need to structure and maintain a useful test suite. |
Python’s unittest documentation covers [cases, fixtures, suites, runners, and discovery](https://docs.python.org/3/library/unittest.html). The [pytest overview](https://docs.pytest.org/en/stable/), [good practices guide](https://docs.pytest.org/en/stable/explanation/goodpractices.html), and [unittest integration guidance](https://docs.pytest.org/en/stable/how-to/unittest.html) explain pytest’s core features and incremental adoption. Selenium’s own guidance says, “No one approach works for all situations”; choose practices for your environment ([Selenium test practices](https://www.selenium.dev/documentation/test_practices/)).
2. Set up an isolated Python test project
Use a virtual environment so test dependencies stay separate from system packages. The following creates a small project and installs pytest. The current pytest overview documents Python 3.10+ or PyPy 3; check its live compatibility page when choosing a supported version for your project.
mkdir python-tests
cd python-tests
python -m venv .venv
# macOS or Linux
. .venv/bin/activate
# Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
Use a conventional layout such as tests/test_math.py. Pytest also supports other layouts; keep the choice consistent and make sure your application package is importable in the environment where tests run.
python-tests/
├── .venv/
├── calculator.py
└── tests/
└── test_calculator.py
3. Write and run tests with unittest
unittest is a sound option when you want a standard-library runner or the project already uses TestCase. Test classes inherit from unittest.TestCase; test methods start with test. Setup and cleanup methods can prepare and release resources.
# calculator.py
def add(a, b):
return a + b
# tests/test_calculator_unittest.py
import unittest
from calculator import add
class AddTests(unittest.TestCase):
def setUp(self):
self.left = 2
def test_adds_numbers(self):
self.assertEqual(add(self.left, 3), 5)
if __name__ == "__main__":
unittest.main()
Run a specific file directly or discover tests from the current directory:
python -m unittest tests/test_calculator_unittest.py
python -m unittest discover -s tests -p "test*.py" -v
Use setUp and corresponding cleanup when each test needs resources. Keep cases independent so they can run alone or in different combinations. The standard library’s discovery conventions and command-line options are documented in [unittest](https://docs.python.org/3/library/unittest.html).
4. Write and run tests with pytest
Pytest tests can be functions, and ordinary Python assertions produce informative failure output. By default, pytest discovers files named test_*.py or *_test.py, and functions or methods named test_*.
# calculator.py
def add(a, b):
return a + b
# tests/test_calculator.py
from calculator import add
def test_adds_numbers():
assert add(2, 3) == 5
python -m pytest
python -m pytest -v
python -m pytest tests/test_calculator.py::test_adds_numbers
Fixtures make setup reusable while keeping dependencies visible in the test signature:
# tests/test_users.py
import pytest
@pytest.fixture
def sample_user():
return {"name": "Ada", "active": True}
def test_user_is_active(sample_user):
assert sample_user["active"] is True
Fixtures can yield a resource and clean it up after the test:
@pytest.fixture
def temporary_connection():
connection = open_connection()
try:
yield connection
finally:
connection.close()
Replace open_connection() with your application’s connection setup. Put shared fixtures in conftest.py when multiple test modules need them. Avoid hidden shared state: tests should be understandable and repeatable independently.
5. Choose a test layout and naming scheme
For a small project, a top-level tests/ directory is easy to navigate. Larger packages can place tests alongside components or split unit, integration, and browser tests into separate directories. Pytest documents both separate and inline layouts. Choose a structure that makes the test’s scope clear, then use it consistently.
- Name files
test_*.pyor*_test.pyfor default pytest discovery. - Name test functions and methods
test_*. - Keep test data and setup local where possible; share fixtures only when sharing makes the tests clearer.
- Use a virtual environment and install the package under test in the same environment used by developers and CI.
See [pytest good practices](https://docs.pytest.org/en/stable/explanation/goodpractices.html) for discovery and project-layout details.
6. Keep or adopt unittest tests incrementally
You do not need to discard an existing unittest suite to use pytest. Pytest supports unittest test cases out of the box, so you can run the current suite with pytest and adopt pytest features in new or updated tests as it makes sense.
python -m pip install pytest
python -m pytest
Keep the existing unittest.TestCase classes intact while you establish pytest discovery and CI. Convert tests only when there is a concrete benefit, such as simpler fixture composition or clearer assertion output. Review [pytest’s unittest integration](https://docs.pytest.org/en/stable/how-to/unittest.html) for compatibility details.
7. Add browser automation for user-visible behavior
Unit tests cannot establish that a page renders correctly or that a user can complete a browser workflow. Selenium and Playwright automate real browser interactions, but browser tests bring browser installation, runtime, and maintenance needs. Decide which browsers and environments matter before building a suite. Neither tool is categorically faster or more reliable for every project.
Selenium with Python
Selenium WebDriver’s Python API works with both unittest and pytest. Modern Selenium uses Selenium Manager to handle browser and driver installation when a WebDriver is instantiated, though the target environment’s browser setup still needs to be checked. Always close the browser, including when an assertion fails.
# pip install selenium pytest
# tests/test_homepage_selenium.py
from selenium import webdriver
from selenium.webdriver.common.by import By
def test_homepage_has_title():
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
assert "Example Domain" in driver.title
assert driver.find_element(By.TAG_NAME, "h1").text == "Example Domain"
finally:
driver.quit()
python -m pytest -v tests/test_homepage_selenium.py
For remote browser sessions, Selenium requires a Selenium Grid. Browser and cross-browser behavior make functional tests challenging; Selenium automates interactions but does not design a good suite for you. Keep tests focused on valuable user flows and isolate setup and cleanup. See the [Selenium Python API](https://www.selenium.dev/selenium/docs/api/py/) and [test practices](https://www.selenium.dev/documentation/test_practices/).
Playwright with Python and pytest
Playwright’s documented CI pattern installs Playwright and its browsers and dependencies, then runs pytest. The example below uses the synchronous API and closes its browser with a context manager.
# pip install playwright pytest
# playwright install
# tests/test_homepage_playwright.py
from playwright.sync_api import sync_playwright
def test_homepage_has_heading():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
try:
page.goto("https://example.com")
assert page.title() == "Example Domain"
assert page.get_by_role("heading", name="Example Domain").is_visible()
finally:
browser.close()
python -m pytest -v tests/test_homepage_playwright.py
Install browser binaries and operating-system dependencies in CI as required by the runner. Playwright documents playwright install --with-deps for CI setup and shows traces retained on failure in a GitHub Actions example. Treat that workflow as an example to adapt, not a requirement for every project. See [Playwright’s Python CI guide](https://playwright.dev/python/docs/ci).
8. Design a maintainable automation suite
- Keep tests independent. Each test should establish the state it needs and clean up its own resources. Avoid order-dependent tests and shared mutable data.
- Separate test scopes. Unit tests should target small code behavior; integration tests exercise connected components; browser tests validate selected end-to-end flows.
- Make setup and teardown explicit. Use unittest setup/cleanup or pytest fixtures. For browser sessions, always quit or close the browser even after failure.
- Keep browser coverage intentional. List the browsers and behaviors the product actually needs. Cross-browser checks add environment and maintenance considerations.
- Make CI reproducible. Pin or constrain dependencies according to your project policy, provision browsers and OS libraries, and preserve useful failure artifacts such as traces where appropriate.
- Prefer actionable assertions. Assert observable outcomes that explain what failed, rather than adding many checks to one opaque workflow.
- Use test data that can be reset. A test should not depend on a developer’s local account, mutable production state, or another test’s side effects.
These are context-dependent practices. Selenium’s guidance explicitly advises applying recommendations to the team’s environment rather than treating any one approach as universal.
9. Run tests in continuous integration
For unit tests, CI needs a supported Python runtime, an isolated dependency install, and a command such as python -m pytest or python -m unittest discover. Browser tests additionally need browsers and their system dependencies available to the agent. A minimal shell sequence for a pytest project is:
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e . pytest
python -m pytest -v
For a project without packaging metadata, install its actual dependencies and configure imports for your chosen layout. For Playwright, install its package and browser dependencies before running pytest:
python -m pip install pytest playwright
python -m playwright install --with-deps
python -m pytest -v
The [Playwright CI documentation](https://playwright.dev/python/docs/ci) gives a GitHub Actions example that collects traces on failure and uploads artifacts. Choose artifacts that help diagnose failures and fit your CI retention policy; the exact workflow depends on your runner.
10. Performance, reliability, and cost
The sources here do not establish a controlled speed or reliability winner between pytest, Selenium, and Playwright. Test scope itself affects runtime: a function test avoids browser startup, while a browser test launches and drives a browser. Keep a fast feedback path for focused tests, and run broader browser coverage where the team’s CI capacity and risk justify it.
- Performance: Run only the relevant test while iterating, then run the full suite before integration. Avoid launching more browser sessions in parallel than the CI agent can support.
- Reliability: Make tests independent, wait for observable conditions rather than arbitrary assumptions, clean up resources, and retain diagnostic artifacts for browser failures.
- Cost: Frameworks are software dependencies; practical costs include developer time, CI minutes, browser infrastructure, and maintaining test environments. No benchmark or monetary comparison is asserted here.
11. Troubleshooting common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| Pytest reports no tests collected | File or test names do not match discovery patterns, or tests are outside the searched path. | Use test_*.py or *_test.py, name tests test_*, and run python -m pytest tests. |
| Import error for the application module | The package is not installed in the active environment, or the project layout is not on the import path. | Activate the project virtual environment, install the package (often python -m pip install -e .), and check the selected layout. |
| A unittest test passes directly but not under discovery | Discovery path or pattern does not include its module, or a test method/class does not follow naming conventions. | Run python -m unittest discover -s tests -p "test*.py" -v and check file and method names. |
| Browser driver or browser cannot start | The CI agent lacks a browser, driver, OS dependency, or a compatible setup. | Confirm browser availability and dependencies. Selenium Manager handles driver/browser setup in modern Selenium, but validate your environment; for remote sessions configure Selenium Grid. |
| Playwright says the browser executable is missing | The Python package is installed, but its browser binaries are not. | Run python -m playwright install locally or python -m playwright install --with-deps in a suitable CI environment. |
| Browser is left running after a failure | Cleanup is not guaranteed on the assertion path. | Use try/finally, context managers, or fixture teardown to close or quit the browser. |
| A browser test is inconsistent across runs | Tests may rely on shared state, timing, network conditions, or an unprepared environment. | Isolate data and browser state, wait for the expected observable condition, and capture diagnostic artifacts to investigate the actual failure. |
12. Or skip the browser setup
For browser screenshot checks and visual review, [ScreenshotNeo](https://screenshotneo.com) offers a screenshot API and MCP server. One GET request returns an image or PDF; see the [API documentation](https://screenshotneo.com/docs/) for parameters and formats.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. This is useful for capturing pages, but it does not replace assertions and interaction tests in a browser automation suite.
Sign up for 1,000 free screenshots a month, with no card required.
13. Frequently asked questions
Can I use pytest and unittest in the same project?
Yes. Pytest can run unittest test cases, which makes gradual adoption practical.
Do I need Selenium or Playwright to test a Python application?
No. Use browser automation when the behavior under test depends on a real browser; ordinary code tests can use unittest or pytest.
Can I use Selenium with pytest?
Yes. Selenium’s Python documentation demonstrates pytest integration as well as unittest integration.
Which framework should a new team choose?
Choose based on scope, existing code, authoring preferences, browser requirements, and CI environment. Pytest is a flexible starting point for many new suites, while unittest is a capable standard-library choice.
Does this guidance establish which browser tool is fastest?
No controlled comparison is included in the source material. Evaluate the browsers, environments, setup, debugging, and test code your project needs.


