Logo OnlineCBT
PYTHON Tutorial
PYTHON · LESSON 47

Testing with pytest

Write focused unit tests with assertions and parametrisation.

Level: Beginner to Advanced
Duration: ~60 Mins Deep-Dive
Updated: 26 Jul 2026

Lesson 47: Testing with pytest

1. Introduction to Software Quality Assurance & Automated Testing

Welcome to Module 8 of our Python Mastery Series: Software Quality Assurance, Testing and Production Deployment! Up to this point in our comprehensive course, we have constructed complex object-oriented architectures, async event loops, custom decorators, and persistent data stores. However, in enterprise software development, writing working functional code is only half the battle.

As software applications evolve through continuous integration and deployment (CI/CD) pipelines, new feature commits or code refactoring frequently introduce silent regressions—breaking existing business logic without developer awareness. Manually running interactive scripts or inspecting terminal output is slow, error-prone, and impossible to scale across enterprise repositories.

To guarantee long-term system reliability, modern software engineering relies on Automated Test Suites. In the Python ecosystem, the industry gold standard for test automation is `pytest`.

In this lesson, we will explore testing strategies (Unit, Integration, and End-to-End), master the pytest test runner mechanics, leverage Python's native assert statement with AST introspection, test exception paths using pytest.raises(), build parameterized multi-input test suites with @pytest.mark.parametrize, and integrate test result assertion dispatchers using Structural Pattern Matching (`match-case`).


2. The Testing Pyramid: Unit, Integration, and E2E Tests

Enterprise quality assurance categorizes automated tests into three distinct architectural layers, forming the Testing Pyramid:

        / \
       /   \        End-to-End (E2E) Tests
      /     \       (Full system UI/API workflows, slowest, highest setup)
     /-------\
    /         \     Integration Tests
   /           \    (Component interactions: DB + API + Storage)
  /-------------\
 /               \  Unit Tests
/-----------------\ (Isolated individual functions/classes, lightning fast, majority share)
    
  • Unit Tests
  • Individual functions, isolated methods, pure algorithms.
  • Milliseconds ($< 10\text{ms}$)
  • Verifying discrete code logic in complete isolation from external networks or disk I/O.
  • Integration Tests
  • Multiple combined modules (e.g., Service + Database Repository).
  • Seconds ($100\text{ms} - 5\text{s}$)
  • Verifying that independent components communicate and transfer data schemas correctly.
  • End-to-End (E2E)
  • Complete application pipeline from user CLI/API input to database disk.
  • Minutes
  • Simulating real-world user scenarios across the fully deployed application stack.
Testing Layer Target Scope Boundary Execution Speed Primary Focus / Objective

3. Getting Started with `pytest` and Test Discovery Rules

While Python includes a legacy built-in testing library (unittest), it requires writing verbose class structures (inheriting from unittest.TestCase) and domain-specific assertion methods (self.assertEqual(), self.assertTrue()).

In contrast, `pytest` allows writing clean, standalone test functions using standard Python functions and plain native assert statements!

1. Automatic Test Discovery Conventions

When you run the command pytest in your terminal, the runner scans your directory tree automatically following strict naming rules:

  • Scans for files named test_*.py or *_test.py.
  • Inside matching files, discovers functions whose identifiers start with test_*().
  • Discovers test classes whose identifiers start with Test* (without an __init__ method).
Executing `pytest` from the Command Line:
# Run all discovered tests across current workspace:
pytest

# Run tests with verbose output showing individual function names:
pytest -v

# Run tests and stop on the first failure encountered (-x flag):
pytest -v -x
        

4. Native `assert` Statements & Failure AST Introspection

In standard Python, an assert condition statement raises a bare AssertionError when the condition is False. However, when executed through pytest, the runner rewrites Python's Abstract Syntax Tree (AST) dynamically.

When an assertion fails under pytest, it provides deep **AST Introspection**—displaying intermediate variable values, data structure differences, list diffs, and dictionary missing keys automatically without writing custom error string code!

TEST_CALCULATOR_CORE.PY
# Source Module Code (Target Business Logic)
def calculate_tax_and_total(base_price: float, tax_rate: float = 0.18) -> dict[str, float]:
    if base_price < 0.0 or tax_rate < 0.0:
        raise ValueError("Price and tax rate must be non-negative!")
    
    tax_amount = round(base_price * tax_rate, 2)
    net_total = round(base_price + tax_amount, 2)
    return {"base": base_price, "tax": tax_amount, "total": net_total}


# =====================================================================
# pytest Test Suite (Following test_* naming conventions)
# =====================================================================

def test_calculate_tax_standard_rate():
    """Unit test: Validates standard 18% tax calculation."""
    result = calculate_tax_and_total(100.0)
    
    # Native Python assert statements evaluated by pytest
    assert result["base"] == 100.0
    assert result["tax"] == 18.0
    assert result["total"] == 118.0

def test_calculate_tax_custom_rate():
    """Unit test: Validates custom tax rate calculations."""
    result = calculate_tax_and_total(200.0, tax_rate=0.05)
    assert result["tax"] == 10.0
    assert result["total"] == 210.0

print("Test Suite File Syntactically Valid. Ready for 'pytest' CLI runner execution.")
OUTPUT
Test Suite File Syntactically Valid. Ready for 'pytest' CLI runner execution.

5. Testing Exceptions with `pytest.raises()`

A robust test suite must verify not only that code succeeds with valid inputs (the Happy Path), but also that code raises the expected exceptions when presented with invalid inputs (the Sad Path).

To test exception handling, pytest provides the context manager: `pytest.raises(ExpectedException)`.

TEST_EXCEPTIONS_DEMO.PY
import pytest

# Target function raising custom exception
class InsufficientFundsError(Exception):
    pass

def process_bank_withdrawal(balance: float, amount: float) -> float:
    if amount <= 0:
        raise ValueError("Withdrawal amount must be strictly positive!")
    if amount > balance:
        raise InsufficientFundsError(f"Cannot withdraw ${amount:.2f}! Balance is ${balance:.2f}.")
    return balance - amount


# =====================================================================
# pytest Exception Test Suite
# =====================================================================

def test_withdrawal_negative_amount_raises_value_error():
    """Verifies that negative withdrawal amounts raise ValueError."""
    with pytest.raises(ValueError) as exc_info:
        process_bank_withdrawal(500.0, -50.0)
    
    # Inspecting exception message text
    assert "strictly positive" in str(exc_info.value)

def test_withdrawal_overdraft_raises_insufficient_funds():
    """Verifies that overdraft amounts raise custom InsufficientFundsError."""
    with pytest.raises(InsufficientFundsError) as exc_info:
        process_bank_withdrawal(100.0, 750.0)
    
    assert "Cannot withdraw $750.00" in str(exc_info.value)

print("Exception Test Suite Defined cleanly. Execution guarded by pytest.raises().")
OUTPUT
Exception Test Suite Defined cleanly. Execution guarded by pytest.raises().

6. Multi-Input Test Suites: `@pytest.mark.parametrize`

Writing separate test functions for dozens of input permutations creates massive code duplication. To solve this, pytest provides the decorator: `@pytest.mark.parametrize("arg_names", [data_tuples])`.

How Parametrization Works: The @pytest.mark.parametrize decorator executes the target test function multiple times automatically—passing each input-output data tuple as individual arguments! If one parameter set fails, pytest reports that specific failure without halting execution of remaining parameters.
TEST_PARAMETRIZE_DEMO.PY
import pytest

# Target utility function to test
def is_valid_user_id(user_id: str) -> bool:
    """Validates user ID format: Must start with 'USR_' and end with 4 digits."""
    if not isinstance(user_id, str):
        return False
    return user_id.startswith("USR_") and len(user_id) == 8 and user_id[4:].isdigit()


# =====================================================================
# Parametrized Test Suite
# =====================================================================

@pytest.mark.parametrize(
    "input_id, expected_result",
    [
        ("USR_1001", True),   # Valid standard ID
        ("USR_9999", True),   # Valid boundary ID
        ("usr_1001", False),  # Invalid lowercase prefix
        ("USR_123", False),   # Invalid length (too short)
        ("USR_ABCD", False),  # Invalid suffix (non-numeric)
        ("ADM_1001", False),  # Invalid prefix name
        (12345678, False),    # Invalid data type (int)
    ]
)
def test_user_id_validation_matrix(input_id, expected_result):
    """Executes 7 distinct test cases automatically using a single test function!"""
    assert is_valid_user_id(input_id) == expected_result

print("Parametrized Test Matrix configured successfully. Covers 7 distinct data permutations.")
OUTPUT
Parametrized Test Matrix configured successfully. Covers 7 distinct data permutations.

7. Custom Markers and Test Filtering

As test suites grow to thousands of tests, running the entire suite on every code save becomes time-consuming. You can tag test functions with custom Markers using @pytest.mark. to group and filter tests during execution.

1. Built-In & Custom Markers Cheat Sheet

  • @pytest.mark.slow
  • Custom tag for slow, resource-heavy integration tests.
  • pytest -m "not slow"
  • @pytest.mark.smoke
  • Custom tag for critical fast smoke tests.
  • pytest -m smoke
  • @pytest.mark.skip(reason="...")
  • Unconditionally **skips** the test during execution runs.
  • Automatically skipped.
  • @pytest.mark.xfail(reason="...")
  • Marks test as **Expected to Fail** (useful for known bugs being tracked).
  • Reported as XFAIL.
Marker Decorator Syntax Execution Behavior / Purpose CLI Selection Flag

8. Combining Test Assertions with `match-case` Pattern Matching

Building automated test result dispatchers involves processing heterogeneous test execution result payloads and using match-case structural pattern matching to parse status codes, assertions, and diagnostic metrics cleanly.

MATCH_CASE_TEST_DISPATCHER.PY
def evaluate_test_report_payload(report_tuple: tuple) -> str:
    """
    Parses test report outcome tuples using Structural Pattern Matching.
    Demonstrates classifying automated test suite metrics dynamically.
    """
    match report_tuple:
        case ("PASSED", test_name, duration_ms) if duration_ms < 50.0:
            return f"FAST_PASS: Test '{test_name}' passed in {duration_ms:.1f}ms"

        case ("PASSED", test_name, duration_ms) if duration_ms >= 50.0:
            return f"SLOW_PASS_WARNING: Test '{test_name}' passed but took {duration_ms:.1f}ms (>50ms target)"

        case ("FAILED", test_name, str(err_type), str(err_msg)):
            return f"TEST_FAILURE_ALERT: '{test_name}' failed with {err_type} -> {err_msg}"

        case ("SKIPPED" | "XFAIL", test_name, reason):
            return f"TEST_BYPASSED: '{test_name}' skipped -> Reason: {reason}"

        case _:
            return f"UNRECOGNIZED_REPORT_SCHEMA: {report_tuple}"

# Testing Pattern Matched Report Dispatcher
print(evaluate_test_report_payload(("PASSED", "test_calculate_tax", 12.4)))
print(evaluate_test_report_payload(("PASSED", "test_database_integration", 145.8)))
print(evaluate_test_report_payload(("FAILED", "test_user_auth", "PermissionError", "Access Denied")))
print(evaluate_test_report_payload(("SKIPPED", "test_legacy_api", "Deprecated in v2.0")))
OUTPUT
FAST_PASS: Test 'test_calculate_tax' passed in 12.4ms
SLOW_PASS_WARNING: Test 'test_database_integration' passed but took 145.8ms (>50ms target)
TEST_FAILURE_ALERT: 'test_user_auth' failed with PermissionError -> Access Denied
TEST_BYPASSED: 'test_legacy_api' skipped -> Reason: Deprecated in v2.0

9. Frequently Asked Interview Questions with Answers

Q1: What is the primary difference between Python's standard `unittest` library and `pytest`?
Answer: unittest requires writing object-oriented class hierarchies inheriting from unittest.TestCase and using specialized assertion methods (e.g., self.assertEqual()). pytest allows writing standalone test functions using native Python assert statements, featuring AST introspection failure reporting, automatic test discovery, and rich fixture systems.
Q2: How does `pytest` discover test files and functions automatically?
Answer: pytest scans the directory structure recursively for files starting with test_*.py or ending with *_test.py. Inside these files, it discovers standalone functions prefixed with test_*() and classes prefixed with Test*.
Q3: How do you verify that a function raises a specific exception in `pytest`?
Answer: By wrapping the function invocation inside a with pytest.raises(ExpectedException) as exc_info: context manager. The exception message and details can then be verified by asserting against exc_info.value.
Q4: What is the purpose of `@pytest.mark.parametrize`?
Answer: @pytest.mark.parametrize allows running a single test function multiple times across a matrix of different argument tuples, eliminating code duplication while testing multiple input-output data permutations.
Q5: What is AST Introspection in `pytest` assertions?
Answer: AST (Abstract Syntax Tree) Introspection is pytest's capability to rewrite failed assert statements during bytecode execution, displaying the exact evaluated values of variables, intermediate expression results, and data structure diffs automatically.

10. Homework & Practical Assignments

Task 1: Unit Test Suite for Password Validation Engine

Create a script named test_password_engine.py inside your lesson_47 folder:

  • Define a function validate_password_strength(password: str) -> bool checking: length >= 8, at least one digit, and at least one uppercase letter. Raise TypeError if input is not a string.
  • Write a parametrized test function using @pytest.mark.parametrize covering 6 distinct valid and invalid password strings.
  • Write a separate test function using pytest.raises(TypeError) verifying non-string input handling.

Task 2: Test Report Dispatcher with Pattern Matching

Create a script named test_report_task.py:

  • Define a test metric analyzer function accepting result tuples: ("UNIT", "PASSED", 10.5), ("INTEGRATION", "FAILED", "TimeoutError"), ("E2E", "SKIPPED", "No DB Connection").
  • Use match-case structural pattern matching to parse test layers and outcomes, returning formatted log strings using f-strings.
  • Execute function across sample tuples and verify outputs.

Task 3: Master Review Capstone Project — Production Enterprise Test Runner & Quality Assurance OS (`qa_test_engine_os.py`)

Create a script named qa_test_engine_os.py inside your lesson_47 folder. This assignment tests and integrates **ALL concepts learned across Lessons 1 through 47**.

Project Architectural Requirements Specification:

  1. pytest Testing Architecture (Lesson 47):
    • Construct a fully unit-tested domain service module containing business logic and corresponding pytest test functions.
    • Utilize pytest.raises() to verify sad-path domain exception scenarios.
    • Utilize @pytest.mark.parametrize for comprehensive data matrix coverage.
  2. Asyncio & Concurrency Integration (Lessons 45-46):
    • Incorporate asynchronous coroutine service calls tested using asyncio.run() wrappers.
    • Offload blocking math tasks to thread/process pools.
  3. Temporal & RegEx Text Processing (Lessons 43-44):
    • Parse UTC-aware execution timestamps using datetime and zoneinfo.ZoneInfo.
    • Pre-compile domain RegEx patterns with Named Groups for extracting test names and execution times from log strings.
  4. Type Hints, Generics & Static Safety (Lesson 42):
    • Apply explicit modern type annotations (X | Y, list[str], dict[str, Any]) throughout all functions and classes.
    • Define domain TypeAlias definitions and Generic Response Envelopes.
  5. Metaprogramming, Closures & Decorators (Lessons 40-41):
    • Build function factories generating customized metric accumulator closures.
    • Build a 3-level configurable decorator audit_test_execution(log_level="INFO") with @functools.wraps.
  6. Iterators, Generators & Functional Tools (Lesson 39):
    • Implement generator functions streaming test log execution lines lazily with $O(1)$ memory footprint.
  7. Full OOP Architecture (Lessons 32-38):
    • Define abstract base class BaseTestCase(ABC) and concrete Data Classes TestRecord and SuiteResult.
    • Build composite manager QualityAssuranceEngineOS composing storage drivers and custom context managers.
  8. Observability & Fault Tolerance (Lessons 29-31):
    • Set up multi-handler logging to console and qa_engine.log file.
    • Incorporate developer assertions (assert) verifying suite invariant non-nullability.
    • Define custom exception hierarchy (QAEngineError, TestAssertionFailureError).
  9. Context Managers & Persistence Layer (Lessons 27-30):
    • Use custom class-based context manager TestVaultLock to manage persistent database lock files.
    • Persist JSON records to test_results.json and export CSV audit reports to qa_audit.csv using pathlib.Path.
  10. Nested Collections & Comprehensions (Lessons 21-26):
    • Maintain in-memory registry dictionaries mapping Test IDs to Data Class Instances.
    • Use List and Dictionary Comprehensions to clean and transform records.
  11. Advanced Function Parameters & Scope (Lessons 15-17):
    • Structure logic into pure modular functions with type hints, docstrings, early return Guard Clauses, and *args / **kwargs logging.
  12. Indefinite & Definite Loops (Lessons 13-14):
    • Run the interactive CLI interface inside a continuous while True menu loop.
    • Iterate through output reports using for loops with enumerate() and .items().
  13. Pattern Matching CLI Dispatcher (Lesson 12):
    • Process user CLI command tokens inside a match-case block with test match guards:
      • case ["RUN", "UNIT", test_id, name] → Execute unit test function and record metrics.
      • case ["PARAMETRIZE", "EVALUATE", *data_args] → Run test suite matrix across input arguments.
      • case ["AUDIT", "FAILURES"] → Parse failed assertions using match-case dispatcher.
      • case ["EXPORT", "CSV"] → Export completed test audit history to CSV.
      • case ["EXIT" | "QUIT"] → Terminate session safely using a sentinel flag.
      • case _ → Output command error message.
  14. Conditionals, Formatting & Foundations (Lessons 1-11):
    • Sanitize all inputs using .strip() and .upper().
    • Format tabular reports using f-strings with field width alignment specifiers (:<15, :>10) and clear visual borders.

11. File & Workspace Directory Structure

Standard Course Workspace Layout:

Ensure all exercise files are stored inside their corresponding lesson directories:

python_mastery_course/
│
├── lesson_01/ ... lesson_46/
│
└── lesson_47/
    ├── test_calculator_core.py
    ├── test_exceptions_demo.py
    ├── test_parametrize_demo.py
    ├── match_case_test_dispatcher.py
    ├── test_password_engine.py      <-- (Task 1)
    ├── test_report_task.py          <-- (Task 2)
    └── qa_test_engine_os.py         <-- (Task 3: Master Review Capstone)
        

12. What We Will Learn Next

Next Up: Lesson 48 — Fixtures, Mocking and Coverage

Now that you master basic test writing, native assertions, exception testing, and parametrization with pytest, we will explore advanced enterprise testing methodologies!

In the next lesson, we will cover:

  • Reusable test state setup & teardown using pytest Fixtures (`@pytest.fixture`).
  • Fixture Scopes: function, class, module, and session.
  • Centralized test configuration sharing using `conftest.py`.
  • Isolating external dependencies (APIs, DBs, File Systems) using Mocking (`unittest.mock` / `mocker`).
  • Measuring codebase test thoroughness using Code Coverage (`pytest-cov`).

📝 Live Lesson Practice

HTML/CSS JavaScript Python C++ C PHP
⌨️ Practice Inputs (लाइव इनपुट भरें) (खाली होने पर RED, भरने पर GREEN underline)
💻 Code Editor (Monaco VS Code Engine)
👀 Live Preview
Address Contact

+91 7877547686

E-mail

onlinecbtportal@gmail.com

Helpline Number

+91 7877547686


Click To Download
Get it on Google Play

ऐप डाउनलोड करने
के लिए Google Play पर
उपलब्ध है

WhatsApp Chat