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.
Choose cases from a requirement, not from the implementation
For the discount rule above, write a small decision table before extending the test suite. A member just below the threshold receives no discount; a member exactly at the threshold qualifies; a non-member above it does not. These cases describe observable business behaviour. Copying the same if expression into the expected result would risk repeating the implementation’s mistake.
Give each failure a useful name. If the threshold accidentally changes from inclusive to exclusive, the threshold case should immediately explain the regression. Include zero and invalid negative input where the public contract requires them. Decide separately whether values such as text, infinity or NaN are supported; adding arbitrary input cases without defining the contract produces confusing expectations.
A fixture that passes once can still leak state
Suppose one test appends a student to a shared list and another expects exactly two students. A module-scoped mutable fixture can make the second test depend on whether the first already ran. A function-scoped fixture that creates a new list for each test isolates those changes. A shallow copy is insufficient when nested dictionaries are also mutated.
For files, pytest’s tmp_path fixture provides a separate temporary directory for each test. Use it for generated reports instead of writing to a fixed filename in the project. Assert the expected file contents and names. Do not let a test pass simply because a previous run left a matching output behind.
Cleanup needs attention when setup itself fails. Pytest does not run code after a yield that was never reached. If a fixture creates two resources and the second creation fails, cleanup written only after yield may miss the first resource. Prefer small fixtures that pair each successful resource acquisition with its own teardown, following pytest’s safe-fixture guidance.
Mock the boundary while keeping the behaviour real
Imagine a function that reads a student record from an external API and creates a local summary. Replace the API call with a controlled response, but still run the real validation and summary logic. Mocking the entire function under test only demonstrates that your mock returns the value you supplied.
Provide a successful response, a missing required field and a dependency error. Verify that each produces the documented result or exception. If the function imports a dependency directly, patch the name where that function looks it up. Patching another module’s original definition may leave the locally imported reference untouched.
Keep assertions on call counts only when they represent a requirement. For example, checking that a failed validation does not send an email is valuable. Requiring an internal helper to be called exactly twice can make an innocent refactor fail even when user-visible behaviour is unchanged.
Read an unexpected failure systematically
Run the failing test by itself, inspect the first relevant traceback and compare actual with expected values. Then run its containing module and the full suite. A failure that appears only in the suite suggests shared state, environment changes or ordering assumptions. Repeatedly rerunning until green hides the symptom without explaining it.
Check whether the failure occurred during setup, the test body or teardown. A database fixture that never connected is different from an incorrect query result. Keep those categories visible when reporting results. A skipped test also differs from a passing test: it supplies no execution evidence for the omitted behaviour.
Use coverage to ask a better question
If every line executes but the suite never checks a returned value, coverage can look healthy while the behaviour remains unprotected. Temporarily introduce a small deliberate mistake in a practice copy, such as changing the threshold comparison, and confirm the relevant test fails. Revert that mistake before continuing. This checks whether the assertion detects the fault you care about.
For a portfolio, record the requirement, representative cases, test command and observed outcome. Explain which external behaviours were simulated and which integration paths remain untested. A short, isolated suite with meaningful assertions is more convincing than a large number of fragile tests with unclear purpose.
Further official references
- pytest fixture teardown and safe structure
- pytest temporary paths
- pytest monkeypatch guidance
- pytest assertion and exception checks
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: