Mastering Pytest: From Basics to Advanced API Automation
This guide explains why pytest surpasses unittest as the most popular Python testing framework, walks through installation, writing and running tests, explores core features such as fixtures, parameterization, markers, and plugins, and demonstrates a complete API‑automation suite with advanced fixture techniques and custom plugin development.
Why Choose pytest?
Compared with the standard unittest library, pytest requires no inheritance, uses simple function names starting with test_, offers concise assert a == b syntax, provides a powerful fixture mechanism with dependency injection, built‑in parameterization via @pytest.mark.parametrize, and an extensive plugin ecosystem (e.g., pytest‑html, pytest‑xdist, pytest‑ordering). Failure retry is supported through the pytest‑rerunfailures plugin.
Quick Start
Installation pip install pytest First test
# test_sample.py
def add(x, y):
return x + y
def test_add():
assert add(1, 2) == 3
assert add(0, 0) == 0Run the test
pytest test_sample.py -v
============================= test session starts ==============================
collected 1 item
test_sample.py::test_add PASSED [100%]
============================== 1 passed in 0.02s =============================Enhanced assertions
When an assert fails, pytest shows the intermediate values:
def test_assert_detail():
a = [1, 2, 3]
b = [1, 4, 3]
assert a == b E assert [1, 2, 3] == [1, 4, 3]
E At index 1 diff: 2 != 4
E Left contains one more item: 3Core Features Explained
Test naming rules
Test files: start with test_ or end with _test (e.g., test_api.py).
Test functions: start with test_.
Test classes: start with Test, and their methods also start with test_.
class TestUserAPI:
def test_login(self):
assert login("user", "pass") == 200Fixture – dependency injection
Fixtures provide resources such as database connections or API clients and can have scopes ( function, class, module, session).
import pytest
@pytest.fixture(scope="session")
def db_connection():
print("创建数据库连接")
conn = create_db_conn()
yield conn
print("关闭数据库连接")
conn.close()
@pytest.fixture
def api_client(db_connection):
client = ApiClient()
client.set_db(db_connection)
return client
def test_create_user(api_client):
resp = api_client.post("/users", json={"name": "Alice"})
assert resp.status_code == 201Parameterization – one test, many data
@pytest.mark.parametrize("username,password,expected", [
("admin", "123456", 200),
("admin", "wrong", 401),
("", "123456", 400),
(None, "123456", 400),
])
def test_login(username, password, expected):
resp = login_api(username, password)
assert resp.status_code == expectedMultiple parameterizations generate a Cartesian product:
@pytest.mark.parametrize("role", ["admin", "user"])
@pytest.mark.parametrize("permission", ["read", "write"])
def test_permission(role, permission):
# 2 * 2 = 4 test cases
passMarkers and grouping
@pytest.mark.slow
def test_heavy_compute():
pass
@pytest.mark.api
@pytest.mark.smoke
def test_login_smoke():
pass
# Run only API tests that are not slow
pytest -m "api and not slow"
# Run smoke or regression tests
pytest -m "smoke or regression"Skip and expected failures
@pytest.mark.skip(reason="暂不执行")
def test_not_ready():
pass
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8+")
def test_feature():
pass
@pytest.mark.xfail(reason="已知缺陷,尚未修复")
def test_known_bug():
assert 1 == 0 # XFAILAPI Automation Practical Example
Assume a simple user‑management API (Flask or similar). Project layout:
api_tests/
├── conftest.py # shared fixtures
├── test_user.py # user‑related API tests
├── test_order.py # order‑related tests
├── utils/
│ ├── api_client.py # wrapper around requests
│ └── data_factory.py # test data generation
└── config.py # environment configurationGlobal fixtures in conftest.py
# conftest.py
import pytest, requests
from config import BASE_URL
@pytest.fixture(scope="session")
def base_url():
return BASE_URL
@pytest.fixture(scope="session")
def session():
"""Session‑level requests.Session handling cookies"""
with requests.Session() as sess:
yield sess
@pytest.fixture
def api_client(session, base_url):
"""Encapsulated API client"""
class ApiClient:
def __init__(self, sess, base):
self.sess = sess
self.base = base
def post(self, path, **kwargs):
return self.sess.post(self.base + path, **kwargs)
def get(self, path, **kwargs):
return self.sess.get(self.base + path, **kwargs)
return ApiClient(session, base_url)
@pytest.fixture
def auth_token(api_client):
resp = api_client.post("/auth/login", json={"username": "admin", "password": "123456"})
assert resp.status_code == 200
token = resp.json()["token"]
api_client.sess.headers.update({"Authorization": f"Bearer {token}"})
return tokenTest cases
# test_user.py
import pytest
@pytest.mark.api
class TestUser:
def test_create_user(self, api_client, auth_token):
"""Create a user (requires login)"""
resp = api_client.post("/users", json={"username": "testuser001", "email": "[email protected]"})
assert resp.status_code == 201
data = resp.json()
assert "id" in data
assert data["username"] == "testuser001"
@pytest.mark.parametrize("field,value", [
("username", ""),
("email", "invalid"),
("username", "a" * 101), # 超长
])
def test_create_user_validation(self, api_client, auth_token, field, value):
"""Parameter validation"""
payload = {"username": "normal", "email": "[email protected]"}
payload[field] = value
resp = api_client.post("/users", json=payload)
assert resp.status_code == 400
def test_get_user_by_id(self, api_client, auth_token):
# First create
resp = api_client.post("/users", json={"username": "getuser", "email": "[email protected]"})
user_id = resp.json()["id"]
# Query
get_resp = api_client.get(f"/users/{user_id}")
assert get_resp.status_code == 200
assert get_resp.json()["username"] == "getuser"Report generation
pip install pytest-html
pytest --html=report.html --self-contained-htmlParallel execution
pip install pytest-xdist
pytest -n auto # auto‑detect CPU coresFailure retry
pip install pytest-rerunfailures
pytest --reruns 3 --reruns-delay 2Advanced Fixture Techniques
Fixture parameterization
@pytest.fixture(params=["mysql", "postgresql"])
def database(request):
db = setup_db(request.param)
yield db
db.cleanup()
def test_query(database):
# runs twice, once with each DB
passUsing @pytest.mark.usefixtures
@pytest.fixture(autouse=True)
def clean_database():
# clear tables before each test
clear_tables()
yield
# optional teardown
@pytest.mark.usefixtures("clean_database")
def test_something():
passDynamic scope fixture
def determine_scope(fixture_name, config):
if config.getoption("--run-slow"):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def heavy_resource():
resource = load_heavy_data()
yield resource
resource.release()Custom Plugin Development
A simple timing plugin demonstrates pytest’s extensibility:
# pytest_time_plugin.py
import pytest, time
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup(item):
item.start_time = time.time()
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_teardown(item):
duration = time.time() - item.start_time
print(f"
{item.name} 耗时 {duration:.4f} 秒")Run with pytest -p pytest_time_plugin.py.
Common Issues & Best Practices
Test isolation : each test should be independent; use fixtures to provide clean data and avoid shared mutable state.
Assertion tricks :
# Expect exception
with pytest.raises(ValueError, match="用户名不能为空"):
create_user(username="")
# Approximate equality
assert 0.1 + 0.2 == pytest.approx(0.3)
# Multiple conditions
assert all(x > 0 for x in [1, 2, 3])Debugging failures :
pytest --pdb # drop into pdb on failure
pytest --lf # run only last failed tests
pytest --ff # run failures first, then others
pytest -x # stop after first failureConclusion
Pytest’s concise syntax, powerful fixture system, and rich plugin ecosystem make it the de‑facto standard for Python testing. This article covered quick start, enhanced assertions, fixture dependency injection, parameterized data‑driven tests, markers, skipping, expected failures, a full API‑automation example, report generation, parallel execution, failure retry, advanced fixture tricks, and custom plugin creation, equipping readers to write, organize, and run efficient regression suites.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
