The Complete Python Pytest Guide: From Beginner to Expert (A–Z)
Unit testing is a fundamental part of building stable and reliable Python applications. Among all testing frameworks available, Pytest has…
The Complete Python Pytest Guide: From Beginner to Expert (A–Z)
Unit testing is a fundamental part of building stable and reliable Python applications. Among all testing frameworks available, Pytest has become the industry favourite thanks to its simplicity, power, and flexibility. Whether you are writing APIs, microservices, data pipelines, machine learning workflows, or AWS Lambda functions, Pytest helps you build clean, isolated, and maintainable tests.
This article is a complete A–Z guide covering every major concept you need to know to master Pytest — from the basics all the way to advanced professional testing strategies used in large production systems.
1. Introduction to Pytest
Pytest is a testing framework that:
- Makes writing tests simple and readable
- Automatically discovers tests
- Supports fixtures, mocking, parametrised tests, and plugins
- Integrates easily with CI/CD pipelines
- Works for both small scripts and large enterprise applications
A basic Pytest test looks like this:
def test_add():
assert add(1, 2) == 3
Pytest automatically finds this test and executes it.
2. Test Naming and Structure
Pytest uses simple rules for test discovery:
- Test files must be named: test_.py or _test.py
- Test functions must start with: test_
Common project structure:
tests/
test_users.py
test_orders.py
conftest.py
src/
app/
This keeps your application code separate from your tests.
3. The Arrange–Act–Assert (AAA) Pattern
The AAA pattern helps keep tests readable:
- Arrange — prepare data, mocks, fixtures
- Act — call the function you want to test
- Assert — verify the output or behaviour
Example:
def test_login_success():
# Arrange
user = User("john", "123")
# Act
result = authenticate(user.username, user.password)
# Assert
assert result is True
Another popular style is Given–When–Then, often used in BDD.
4. Fixtures — Reusable Test Setup
Fixtures are the core of Pytest. They allow you to:
- Reuse setup logic
- Keep tests clean
- Provide data or objects to many tests
Example fixture:
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "role": "admin"}
Use the fixture by adding the name as a function argument:
def test_user_role(sample_user):
assert sample_user["role"] == "admin"
5. Fixture Scopes
Pytest supports different fixture scopes:
- function — created for each test (default)
- class — created once per class
- module — created once per module
- session — created once per test run
Example:
@pytest.fixture(scope="module")
def db():
return connect_to_fake_db()
6. conftest.py — Shared Fixtures Across Files
The conftest.py file allows sharing fixtures across multiple test files without importing them.
Example file:
# conftest.py
import pytest
@pytest.fixture
def token():
return "secure-token"
Any test in that directory can use the token fixture automatically.
7. Setup and Teardown
Pytest supports setup and teardown using fixtures:
@pytest.fixture
def connection():
conn = create_db_connection()
yield conn
conn.close()
The yield keyword separates setup and teardown phases.
8. Parametrised Tests
Parametrisation allows running the same test with multiple inputs:
import pytest
@pytest.mark.parametrize(
"a, b, expected",
[
(1, 2, 3),
(5, 5, 10),
(-1, 1, 0)
]
)
def test_add(a, b, expected):
assert add(a, b) == expected
This reduces duplication and increases coverage.
9. Mocking and Patching
Mocking is essential for isolating code and avoiding real external calls.
Common reasons to mock:
- Network requests
- Database connections
- File I/O
- External APIs
Pytest works seamlessly with Python’s unittest.mock.
Patching a function:
from unittest.mock import patch
@patch("app.email.send_email")
def test_email(mock_send):
mock_send.return_value = True
assert send_notification() is True
You can use patch as:
- A decorator
- A context manager
10. Mock vs MagicMock
Mock:
- Basic mock object
- Simple attribute stubbing
MagicMock:
- Supports Python magic methods (len, iter, str, etc.)
- Useful for mocking complex behaviour
11. Monkeypatching (Pytest’s Built-in Solution)
Monkeypatch allows modifying:
- Environment variables
- Functions
- Object attributes
Example:
def test_env(monkeypatch):
monkeypatch.setenv("MODE", "test")
Pytest reverts changes automatically after the test.
12. Mocking AWS Services (moto)
Moto allows you to mock AWS services without accessing real AWS.
Example:
from moto import mock_dynamodb
import boto3
@mock_dynamodb
def test_dynamodb():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(...)
Moto supports DynamoDB, S3, SQS, SNS, Lambda, Step Functions, and more.
13. Environment Variable Testing
Environment variables drive configuration for many apps.
You can mock them using:
- monkeypatch
- patch.dict
Example:
import os
def test_env(monkeypatch):
monkeypatch.setenv("DEBUG", "true")
assert os.environ["DEBUG"] == "true"
14. Exception Testing
Use pytest.raises to check error handling:
import pytest
def test_raises():
with pytest.raises(ValueError):
risky_function()
You can also validate the message:
with pytest.raises(ValueError) as e:
risky_function()
assert "Invalid" in str(e.value)
15. Spy vs Mock vs Stub
Mock — replaces the object completely Stub — provides simple return values Spy — wraps real logic and records calls
Use cases:
- Use a mock when you cannot run the real dependency
- Use a stub when you only need predefined data
- Use a spy when verifying behaviour
16. Pytest Markers
Markers help categorise tests:
@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.smoke
Example of skipping a test:
@pytest.mark.skip(reason="Not implemented yet")
def test_feature():
pass
Conditional skip:
@pytest.mark.skipif(sys.platform == "win32", reason="Windows not supported")
17. Capturing Logs (caplog)
Useful for validating logging behaviour:
def test_logging(caplog):
logger.info("Process started")
assert "started" in caplog.text
18. Capturing stdout and stderr (capsys)
Example:
def test_output(capsys):
print("Hello")
captured = capsys.readouterr()
assert "Hello" in captured.out
19. Temporary Files and Directories (tmp_path)
Pytest provides built-in fixtures:
def test_file(tmp_path):
file = tmp_path / "data.txt"
file.write_text("hello")
assert file.read_text() == "hello"
20. Testing Async Code
Use the pytest-asyncio plugin:
import pytest
@pytest.mark.asyncio
async def test_async():
result = await async_function()
assert result == 42
Essential for FastAPI, async AWS Lambda, Redis, etc.
21. Parallel Test Execution (pytest-xdist)
To speed up large test suites:
pytest -n auto
This uses multiple CPU cores to run tests in parallel.
22. Pytest Plugins (Essential in Production)
Some widely used plugins:
- pytest-cov — for coverage
- pytest-xdist — parallel execution
- pytest-mock — simpler mocking
- pytest-timeout — detect hanging tests
- pytest-randomly — find hidden dependencies
- pytest-asyncio — async support
- pytest-django — for Django apps
- pytest-sugar — better test output
23. Snapshot Testing
Useful for:
- JSON API responses
- HTML or template output
- Large quoted data
Snapshots reduce false positives and keep tests stable.
24. Test Ordering and Dependencies
Tools like pytest-order allow explicit ordering.
Example:
@pytest.mark.order(1)
def test_create(): ...
@pytest.mark.order(2)
def test_read(): ...
25. Code Coverage (pytest-cov)
Run with coverage:
pytest --cov=src --cov-report=term-missing
Reports uncovered lines and functions.
Coverage Types:
- Line coverage
- Branch coverage
- Conditional coverage
26. Integrating Pytest with CI/CD
Pytest works seamlessly with:
- GitHub Actions
- GitLab CI
- Jenkins
- AWS CodeBuild
- Azure DevOps
Typical CI command:
pytest -q --cov
27. Best Practices for Writing High-Quality Tests
- Keep tests small and isolated
- Avoid network/database access in unit tests
- Use fixtures for setup, not inside tests
- Mock external APIs
- Prefer parametrised tests for multiple inputs
- Use descriptive test names
- Follow the AAA structure
- Keep tests deterministic
- Clean up with fixtures or temporary dirs
28. Common Mistakes to Avoid
- Overusing mocks
- Testing internal implementation instead of behaviour
- Running slow integration services in unit tests
- Mixing integration tests with unit tests
- Not using conftest.py for shared fixtures
- Relying on global state
29. Summary
Pytest is the most capable, flexible, and developer-friendly testing framework in Python. Once you understand fixtures, parametrisation, mocking, monkeypatching, and plugins, you can confidently build professional-grade test suites suitable for any scale — from small scripts to large cloud platforms.
This A–Z guide covers every major concept you need to move from beginner to expert, equipping you with the knowledge required to write clean, maintainable, and production-ready tests.
메타데이터
- post_id
- 5d721921e00a
- slug
- the-complete-python-pytest-guide-from-beginner-to-expert-a-z-5d721921e00a
- url
- https://medium.com/@amir-saeed/the-complete-python-pytest-guide-from-beginner-to-expert-a-z-5d721921e00a
- canonical_url
- https://medium.com/@amir-saeed/the-complete-python-pytest-guide-from-beginner-to-expert-a-z-5d721921e00a
- author_url
- https://medium.com/@amir-saeed
- status
- ok
- fetched_at
- 2026-07-22 21:01:25