← Back to list

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…

Amir Saeed · 2025-12-12 10:37 · 0 claps · 4.5 min read
#pytest-unit-testing #python-testing-framework #pytest-fixtures-tutorial #mocking-in-pytest #python-test-automation
Open on Medium ↗

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:

  1. Arrange — prepare data, mocks, fixtures
  2. Act — call the function you want to test
  3. 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