Layer 5 Session 33 Testing

Testing Functions and Return Values

We systematically test return values across the project, including edge cases: empty inputs, boundary values, and the exact examples used in each session's own concept explanation.

assert result == expected

Normal cases, edge cases, and the boundary in between.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

1. Learning Objective

By the end of this session you will be able to:

  • Write multiple test functions covering a normal case, an edge case, and a boundary case for one function
  • Use pytest parametrization to run the same test logic across several inputs
  • Test a list comprehension's output by checking exact contents, not just length
  • Test the from_dict classmethod from Session 18 with both valid and malformed input
  • Identify what "edge case" means concretely, using examples from this project

2. Pre-Coding Quiz

Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ€” that is the point. You need 4/5 to proceed to the lab.

Question 1 of 5

For a function find_by_region(countries, region), what is an example of an "edge case" worth testing, beyond the normal "region exists with matches" case?

Edge cases are inputs at the boundary of normal usage: zero matches, an empty source collection, or unusual-but-valid inputs. These are exactly where bugs like accidentally returning None instead of [] tend to hide (recall Session 17).
Question 2 of 5

What does @pytest.mark.parametrize let you do?

@pytest.mark.parametrize decorates a test function with a list of input/expected pairs, and pytest runs the same test body once per pair โ€” avoiding duplicated test functions that differ only in their specific values.
Question 3 of 5

You want to test that [c.name for c in explorer.find_by_region("Africa")] returns exactly ["Kenya", "Ghana"]. Why is asserting the exact list better than just asserting len(result) == 2?

A length-only check would pass even if find_by_region accidentally returned two European countries instead of two African ones โ€” checking exact contents is what actually verifies the filtering logic works correctly.
Question 4 of 5

Testing Country.from_dict() with a dict missing the "population" key should do what?

Malformed input is exactly the kind of edge case worth testing explicitly โ€” pytest.raises(TypeError) confirms the failure mode is the expected, controlled one (a clear TypeError) rather than something confusing happening downstream.
Question 5 of 5

Why is it valuable for a test to use the EXACT same example from a session's own concept explanation, rather than an unrelated new example?

This connects directly to the feedback that shaped this whole course: a test built from the exact material just taught verifies the real behavior matches what was claimed, rather than testing an disconnected, arbitrary scenario.

3. The Concept โ€” Testing Return Values Systematically

TEST BODYwritten oncePARAMETRIZE4 casesrun automatically

One test body, run automatically once per parametrized case โ€” no copy-pasted near-duplicate test functions.

Three kinds of cases: normal, edge, and boundary

A thorough test suite for a function covers its normal, expected use; edge cases (empty inputs, zero matches); and boundary cases (values right at a validation limit, like exactly 0).

# country.py additions
def find_by_region(countries, region):
    return [c for c in countries if c.region == region]

# tests/test_find_by_region.py
from country import Country, find_by_region

def test_find_by_region_normal_case():
    countries = [
        Country(name="Kenya", region="Africa", population=54000000),
        Country(name="Peru", region="Americas", population=33000000),
    ]
    result = find_by_region(countries, "Africa")
    assert [c.name for c in result] == ["Kenya"]  # exact contents, not just length

def test_find_by_region_no_matches_returns_empty_list():
    countries = [Country(name="Kenya", region="Africa", population=54000000)]
    result = find_by_region(countries, "Antarctica")
    assert result == []  # Session 17's guarantee, verified

def test_find_by_region_empty_source_list():
    result = find_by_region([], "Africa")
    assert result == []

Parametrizing repetitive test logic

When the same test logic needs to run against several input/output pairs, @pytest.mark.parametrize avoids copy-pasting near-identical test functions.

import pytest
from country import Country

@pytest.mark.parametrize("value,expected_valid", [
    (54000000, True),
    (0, True),         # boundary โ€” exactly zero should be valid
    (-1, False),        # boundary โ€” just below zero should be invalid
    (-999999, False),
])
def test_set_population_validity(value, expected_valid):
    k = Country(name="Kenya", region="Africa", population=1)
    if expected_valid:
        k.set_population(value)
        assert k.population == value
    else:
        with pytest.raises(ValueError):
            k.set_population(value)

Testing from_dict with valid and malformed input

Session 18's classmethod is a natural place for edge-case testing: what happens with correct data, and what happens with a missing required field.

import pytest
from country import Country

def test_from_dict_builds_correct_instance():
    data = {"name": "Kenya", "region": "Africa", "population": 54000000}
    k = Country.from_dict(data)
    assert k.name == "Kenya"
    assert k.population == 54000000

def test_from_dict_missing_field_raises():
    data = {"name": "Ghost Nation"}  # missing region and population
    with pytest.raises(TypeError):
        Country.from_dict(data)

4. Lab

Lab objective: Write a systematic test suite covering normal, edge, and boundary cases for find_by_region, from_dict, and set_population, using parametrization.

What you will build

A file called tests/test_return_values.py, building on Session 32's country.py.

Step-by-step instructions

1

Add find_by_region and from_dict to country.py if not already present

# country.py (additions)
def find_by_region(countries, region):
    return [c for c in countries if c.region == region]

class Country:
    # ... existing __init__, summary, set_population, grow_population ...

    @classmethod
    def from_dict(cls, data):
        return cls(**data)
2

Write normal-case and exact-content tests for find_by_region

# tests/test_return_values.py
import pytest
from country import Country, find_by_region

def test_find_by_region_returns_exact_matches():
    countries = [
        Country(name="Kenya", region="Africa", population=54000000),
        Country(name="Ghana", region="Africa", population=31000000),
        Country(name="Peru", region="Americas", population=33000000),
    ]
    result = find_by_region(countries, "Africa")
    assert [c.name for c in result] == ["Kenya", "Ghana"]
3

Add edge case tests: no matches, empty source

def test_find_by_region_no_matches():
    countries = [Country(name="Kenya", region="Africa", population=54000000)]
    assert find_by_region(countries, "Antarctica") == []

def test_find_by_region_empty_source():
    assert find_by_region([], "Africa") == []
4

Add from_dict tests, valid and malformed

def test_from_dict_valid():
    k = Country.from_dict({"name": "Kenya", "region": "Africa", "population": 54000000})
    assert k.name == "Kenya"
    assert k.population == 54000000

def test_from_dict_missing_field_raises():
    with pytest.raises(TypeError):
        Country.from_dict({"name": "Ghost Nation"})
5

Add a parametrized boundary test for set_population

@pytest.mark.parametrize("value,should_succeed", [
    (54000000, True),
    (0, True),
    (-1, False),
])
def test_set_population_boundaries(value, should_succeed):
    k = Country(name="Kenya", region="Africa", population=1)
    if should_succeed:
        k.set_population(value)
        assert k.population == value
    else:
        with pytest.raises(ValueError):
            k.set_population(value)

# Run: pytest -v   (the -v flag shows each parametrized case individually)

5. Expected Files Changed

FileActionWhy
country.py Modified Adds find_by_region and from_dict if not already present.
tests/test_return_values.py Created Systematic normal/edge/boundary test coverage, including parametrized cases.
docs/sessions/session-33/index.html Created This session document.
If you find yourself editing any other file, stop. This session touches exactly 3 files.

6. Commit Checkpoint

Once the lab is complete and you can explain every line, make this exact commit:

git add country.py tests/test_return_values.py docs/sessions/session-33/index.html
git commit -m "session-33: systematically test return values, including edge and boundary cases"
Do not commit until you can answer out loud: "Why does test_find_by_region_returns_exact_matches check the exact list of names instead of just the count?"

7. Code Review Checklist

Go through your code line by line and check each item:


8. Post-Coding Quiz

Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.

Question 1 of 5

For a function find_by_region(countries, region), what is an example of an "edge case" worth testing, beyond the normal "region exists with matches" case?

Edge cases are inputs at the boundary of normal usage: zero matches, an empty source collection, or unusual-but-valid inputs. These are exactly where bugs like accidentally returning None instead of [] tend to hide (recall Session 17).
Question 2 of 5

What does @pytest.mark.parametrize let you do?

@pytest.mark.parametrize decorates a test function with a list of input/expected pairs, and pytest runs the same test body once per pair โ€” avoiding duplicated test functions that differ only in their specific values.
Question 3 of 5

You want to test that [c.name for c in explorer.find_by_region("Africa")] returns exactly ["Kenya", "Ghana"]. Why is asserting the exact list better than just asserting len(result) == 2?

A length-only check would pass even if find_by_region accidentally returned two European countries instead of two African ones โ€” checking exact contents is what actually verifies the filtering logic works correctly.
Question 4 of 5

Testing Country.from_dict() with a dict missing the "population" key should do what?

Malformed input is exactly the kind of edge case worth testing explicitly โ€” pytest.raises(TypeError) confirms the failure mode is the expected, controlled one (a clear TypeError) rather than something confusing happening downstream.
Question 5 of 5

Why is it valuable for a test to use the EXACT same example from a session's own concept explanation, rather than an unrelated new example?

This connects directly to the feedback that shaped this whole course: a test built from the exact material just taught verifies the real behavior matches what was claimed, rather than testing an disconnected, arbitrary scenario.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. Why does testing the boundary value 0 for set_population matter specifically, given the validation rule is value < 0?
  2. Could a bug exist where find_by_region returns the correct COUNT of countries but the WRONG countries? Design a scenario where only an exact-content test would catch it.
  3. How did parametrizing the set_population boundary test compare, in terms of clarity, to writing three separate test functions?
  4. Which of today's tests would have caught a real bug from an earlier session, if that bug had existed?

10. What Breaks If This Knowledge Is Missing?

  • False confidence from weak assertions: A test suite full of length-only or existence-only checks gives a false sense of safety โ€” it can pass while the actual returned data is subtly wrong, exactly the gap exact-content assertions close.
  • Testing state changes (Session 34): This session focused on return VALUES. The next session tests state CHANGES on an object over time โ€” a related but distinct testing skill, since not every method returns something meaningful; some just mutate.
  • Regression safety for the whole project: Every function and method tested in this session is now protected against silent regressions in every remaining session of the curriculum โ€” this is the payoff of investing in test coverage now.

11. What We Learned

Python concept mastered: Systematic testing of return values โ€” normal, edge, and boundary cases, exact-content assertions, and pytest parametrization.

Unlocks: The project's core data-returning functions are now protected by a thorough, systematic test suite, not just a handful of happy-path checks.

Next session: Session 34 โ€” Testing Classes and State Changes. We test the other half of the picture: methods that change an object's state over time, rather than returning a value.