Python course module guide: Testing and debugging
Pytest helps Python learners turn expected behavior into repeatable tests. Good tests do more than confirm a happy path: they cover boundaries, failures and dependency behavior while remaining fast and understandable.
What you will learn
- Organize tests with arrange, act and assert.
- Reuse setup through focused fixtures.
- Cover cases with parametrization.
- Replace external dependencies safely.
What a unit test should prove
A unit test checks a small behavior with controlled inputs and a clear expected result. The goal is not to call every line once. The goal is to make important behavior verifiable, including boundaries and failures. Tests should be fast enough to run during development.
def calculate_discount(total: float, member: bool) -> float:
if total < 0:
raise ValueError("total cannot be negative")
rate = 0.10 if member and total >= 1000 else 0
return round(total * rate, 2)
Your first pytest test
from discounts import calculate_discount
def test_member_receives_discount_at_threshold():
result = calculate_discount(1000, member=True)
assert result == 100
Pytest discovers files and functions that follow its naming conventions. The test has one visible reason to fail. If an assertion fails, read the values and traceback using the workflow in the Python debugging guide.
Arrange, act and assert
Arrange creates the inputs and dependencies. Act performs the behavior. Assert checks the result or observable effect. You do not need comments for tiny tests, but this mental structure prevents setup, execution and verification from becoming tangled.
Use parametrization for a behavior table
import pytest
from discounts import calculate_discount
@pytest.mark.parametrize(
("total", "member", "expected"),
[
(999, True, 0),
(1000, True, 100),
(2500, True, 250),
(2500, False, 0),
],
)
def test_discount_rules(total, member, expected):
assert calculate_discount(total, member) == expected
Parametrization is useful when several inputs express the same rule. Give complex cases identifiers or split them into separate tests when a table becomes difficult to understand.
Test expected exceptions
def test_negative_total_is_rejected():
with pytest.raises(ValueError, match="cannot be negative"):
calculate_discount(-1, member=True)
Testing only valid input leaves the error contract undocumented. For domain-specific failure types, study Python custom exceptions and error chaining.
Fixtures for reusable setup
import pytest
@pytest.fixture
def sample_students():
return [
{"name": "Asha", "score": 82},
{"name": "Ravi", "score": 74},
]
def test_student_average(sample_students):
total = sum(item["score"] for item in sample_students)
assert total / len(sample_students) == 78
A fixture should provide a useful test dependency, not hide half the scenario. Start with function scope. Broader scopes can improve speed for expensive setup, but shared mutable state can make tests influence one another.
Replace external behavior with monkeypatch
def get_region():
import os
return os.getenv("APP_REGION", "local")
def test_region_from_environment(monkeypatch):
monkeypatch.setenv("APP_REGION", "test")
assert get_region() == "test"
The monkeypatch fixture automatically restores changes after the test. Patch the name used by the module under test, especially when a dependency was imported directly. Avoid real network calls in unit tests; they are slower and can fail for reasons unrelated to your code.
Test a Flask endpoint
Flask provides a test client so endpoints can be checked without starting a real server. The Flask REST API with SQLite project shows how unit and integration tests fit around validation, JSON responses and database setup.
Coverage is a clue, not a score
Coverage reports show which code did not run during tests. They cannot prove that assertions are meaningful or requirements are correct. Use missing lines to find untested behavior, then add valuable scenarios. Do not write empty assertions merely to raise a percentage.
Recommended project layout
project/
├── pyproject.toml
├── src/
│ └── training_app/
└── tests/
├── test_discounts.py
└── test_validation.py
Install project and test dependencies in an isolated environment. The Python virtual environments and pip guide explains why this keeps versions reproducible.
Common testing mistakes
- Testing implementation details instead of public behavior.
- Using one large test for many unrelated rules.
- Allowing tests to depend on execution order.
- Calling live APIs from unit tests.
- Sharing mutable fixture data between tests.
- Chasing coverage while ignoring edge cases and assertions.
Practice plan
Choose one existing function. Write a happy-path test, a boundary test and a failure test. Convert repeated examples into a parametrized table. If the function reads time, environment data or a network dependency, replace that dependency in the test. Run the suite after each small code change.
Pytest FAQs
Where should tests be stored?
A top-level tests directory is common and keeps application code separate. Follow one predictable layout throughout the project.
Are fixtures only for databases?
No. Fixtures can provide small sample objects, temporary paths, configuration or clients. Use them when setup is meaningful and reusable.
Should private functions be tested directly?
Prefer testing public behavior. Direct private-function tests can make refactoring costly, although a complex isolated algorithm may justify focused coverage.
Official reference: official pytest documentation.
Learn Python with guided practice in Visakhapatnam
These concepts become useful when you apply them in exercises, assignments and reviewed projects. Explore Python training in Vizag for the complete curriculum, classroom and online learning options, and current batch details.
Python module learning path
Continue through the related practical guides in this course-module series: