Skip to main content

Section 1.3 Examples & Tests

Subsection 1.3.1 Overview and Objectives

In the previous sections, you learned about defining data (Step 1) and writing clear method signatures and purpose statements (Steps 2) as part of the Design Recipe. Now, we move on to Step 3: Writing Examples & Tests.
The key idea is straightforward: after clarifying what your function should do, you create a few concrete examples to confirm it behaves correctly. Then, you transform these examples into automated tests, so your checks can be repeated whenever you modify or extend the code.

Subsection 1.3.2 Why Write Examples & Tests?

Each time you write a function—like calculate_total_price or count_vowels—you probably have one or two examples in mind:
  • "If I call it on [2, 3], I expect 5."
  • "If I call it on "" (empty string), I expect 0."
These examples clarify the function’s behavior in specific situations. When you turn them into automated tests, you can re-check them quickly every time you change your code. This prevents a common headache: "I fixed a bug in one place, but I accidentally broke something else!"
In a small project you might get away with ad-hoc or manual testing, but in larger or long-lived projects, automated tests save a lot of time and frustration.

Subsection 1.3.3 A New Example: Counting Words

Let’s illustrate with a small, self-contained function: count_words. Suppose it:
  1. Takes a string of text
  2. Splits it into words separated by spaces
  3. Returns the number of words
For instance:
  • count_words("Hello world") should return 2.
  • count_words("") (empty string) should return 0.
  • count_words("Java is awesome") should return 3.
Let’s write a rough version of the function:
Listing 1.3.1.
def count_words(text):
"""
Returns the number of words in the given text. Words are separated by spaces.

Args:
text (str): A string that may be empty or contain multiple words separated by spaces.
Returns:
int: How many words are in 'text'.
"""
# Split by spaces, filter out empty items, then count
if text.strip() == "": # If it's all whitespace or empty
return 0
words = text.split(" ")
return len(words)
We think this works for normal inputs, but we should confirm with examples—and then automate those examples as tests.

Subsection 1.3.4 Writing Examples with a Table

In the Design Recipe, you create examples right after you define your function’s contract. That way, the function’s behavior is explicit before you dive into coding (or at least very early). Instead of writing these examples as a quick bullet list, we can arrange them in a table that clearly shows inputs, expected outputs, and a brief rationale.
For count_words, we might define:
Table 1.3.2. Examples of count_words Function Behavior
Case Type Input Expected Output Notes
Normal usage "Hello world" 2 Two words separated by one space
Empty string "" 0 No content, so no words
Whitespace only " " 0 Spaces but no actual words
Multiple words " Java is awesome" 3 Splitting by space yields 3 tokens
Each row is a distinct scenario. The Case Type column labels normal, edge, or error cases; the Input and Expected Output columns specify exactly what we feed the function and what we want back; and the Notes column briefly explains the reasoning or highlights potential pitfalls.
A table like this makes it easy to see if you’ve covered all the important situations. For instance, if you left out "" (the empty string), you might miss that edge case.

Subsection 1.3.5 Turning Table Rows into Tests

Once you have your table of examples, the next step is to turn each row into an automated test in Python. Typically, this involves writing a test function that uses assert statements. For our count_words table, here’s one approach:
Listing 1.3.3.
def test_count_words():
# "Hello world" -> 2
assert count_words("Hello world") == 2

# "" (empty string) -> 0
assert count_words("") == 0

# " " (whitespace only) -> 0
assert count_words(" ") == 0

# " Java is awesome" -> 3
assert count_words(" Java is awesome") == 3

print("test_count_words PASSED")
Each assert corresponds to one row in our table. If all of them pass, we’ll see test_count_words PASSED. If any assert fails, Python raises an AssertionError, telling us which scenario isn’t behaving as expected.

Subsection 1.3.6 Seeing an Assertion Fail

Suppose we introduced a bug. For example, if we accidentally returned len(words) - 1 in count_words:
Listing 1.3.4.
def count_words(text):
# Imagine we made a silly bug here:
if text.strip() == "":
return 0
words = text.split(" ")
return len(words) - 1 # BUG: subtract 1 for no reason
Now, when you call test_count_words(), Python raises an AssertionError for any test expecting 2 or 3 words but getting 1 or 2. You might see:
Listing 1.3.5.
AssertionError
This indicates count_words("Hello world") returned 1 instead of 2. By printing out intermediate values or carefully reviewing the code, we can spot the bug and fix it quickly.

Subsection 1.3.7 A Simple "Test Runner"

As your code grows, you might have more than one test function—like test_count_words or test_calculate_total_price. Running them one at a time can get tedious. A small trick is to gather them in a run_tests() function and call them all:
Listing 1.3.6.
def run_tests():
# Call each test function here
test_count_words()
# test_calculate_total_price()
# test_any_other_function()

if __name__ == "__main__":
run_tests()
If everything passes, you’ll see each test’s “PASSED” message. If any fail, AssertionError pops up, telling you which function to fix first.

Subsection 1.3.8 Connection to the Design Recipe

In the broader Design Recipe, these examples and tests come right after you’ve stated each function’s data definitions and purpose. The tests:
  • Reinforce the contract: If your docstring says count_words("Hello world") returns 2, your test enforces that promise.
  • Catch edge-case bugs: We specifically tested empty strings and trailing spaces, which might be overlooked otherwise.
  • Enable quick iteration: Anytime we modify count_words, we re-run the tests to confirm it still works.
Notice how the table-based examples reflect all these considerations, making it easier to confirm your function meets the contract under varied conditions.

Subsection 1.3.9 Looking Ahead: Beyond assert

For larger projects, manually writing your own test functions is still helpful, but Python also provides powerful tools like unittest and pytest:
  • They can automatically discover test functions (so you don’t have to write a run_tests manually).
  • They give more detailed error messages when tests fail.
  • They support advanced features like grouping tests or running only certain ones.
Later in the course, we’ll show you how to import and use pytest once you’re more comfortable with Python modules, error handling, and exceptions. For now, using simple assert checks in a single file is an excellent way to build testable, reliable code without overwhelming complexity.

Subsection 1.3.10 Exercises & Checkpoints

Checkpoint 1.3.7. 1. Write and Test count_letters with a Table.

Create a function named count_letters(text) that returns how many letters (a–z or A–Z) appear in the given string. Then, build a test table covering at least four scenarios (e.g., typical input, all digits, empty string, string with punctuation). Convert each row into a simple assert-based test.

Checkpoint 1.3.8. 2. Edge Cases for count_letters.

Think of one “weird” input for count_letters that might reveal a bug (for instance, a string with emojis or a long string of whitespace). Add an assert test for it and see if your code handles it correctly.

Checkpoint 1.3.9. 3. Debugging a Broken Test.

Intentionally break count_letters by skipping the last letter or ignoring uppercase letters. Run your tests to see which one fails first. Fix the bug and verify the tests pass again.

Checkpoint 1.3.10. 4. Reflect on the Process.

In a short paragraph, describe how it feels to have automated tests for your functions. Do you find bugs more quickly? Do you have more or less confidence when changing your code? Write down your reflections.

Subsection 1.3.11 Conclusion & Next Steps

By turning your function examples into automated tests—even using Python’s simple assert—you gain powerful protection against accidental mistakes and regressions. This practice fits neatly into the Design Recipe: once you declare a function’s purpose, data definitions, and contract, you transform your examples into quick checks that confirm you’re on the right track.
In the next section, we’ll take a closer look at invalid inputs and how to handle them. You’ve already seen Python raise an AssertionError when something goes wrong; soon, you’ll learn to raise and handle other exceptions more gracefully, making your programs more robust.

Subsection 1.3.12 Examples & Tests Exercises

Checkpoint 1.3.11. Parsons: Audrey’s Realization on Examples & Tests.

Rearrange these story beats to see how Audrey learned that writing examples & tests early can prevent major headaches.

Checkpoint 1.3.12. Parsons: Building a Simple test_count_words.

Rearrange these lines to create a coherent test_count_words function that checks typical and edge cases for count_words.
Solution.
Reordered lines form a standard test function:
def test_count_words():
    # Typical case
    assert count_words("Hello world") == 2
    
    # Edge case: empty string
    assert count_words("") == 0
    
    # Edge case: string with extra spaces
    assert count_words("  OpenAI   is   awesome  ") == 3
    
    print("test_count_words PASSED")

Checkpoint 1.3.13. Why Create Examples First?

This section highlights creating examples before (or during) implementation. Which reason below best explains why?
  • Having examples clarifies expected behavior, making it easier to spot logic errors and handle edge cases before writing all the code.
  • Correct! Writing examples first ensures we know exactly what the function should do, simplifying implementation and testing.
  • It automatically generates a user interface with no coding needed.
  • Examples do not build a UI; they just clarify behavior and guide testing.
  • It makes your program run 10× faster.
  • Speed is not the primary concern here; correctness and clarity are the benefits.
  • Python disallows writing code before you define at least three examples.
  • Python doesn’t enforce example-driven development; it’s just a best practice, not a language rule.

Checkpoint 1.3.14. All Tests Pass = No Bugs?

    Once all your tests pass, you have 100% certainty there are no bugs left in the code.
  • True.

  • Even thorough testing can’t guarantee absolute freedom from all bugs. Tests reduce risk but don’t prove total correctness.
  • False.

  • Even thorough testing can’t guarantee absolute freedom from all bugs. Tests reduce risk but don’t prove total correctness.

Checkpoint 1.3.15. Short Answer: Designing Examples & Tests for update_rating.

Imagine we have already created a Data Definition (Section 1) and a Method Signature & Purpose Statement (Section 2) for update_rating(library, song_title, new_rating). Here are the deliverables from previous steps:
Data Definition (from Section 1):
A Song is a Python list of the form [title, rating, play_count], where:
  • title is a string (non-empty).
  • rating is an integer in [1..5].
  • play_count is a nonnegative integer (0 or more).
Method Signature & Purpose (from Section 2):
def update_rating(library: list, song_title: str, new_rating: int) -> bool:
  • Purpose Statement: Updates the rating of the song titled song_title in ibrary if it exists, returning True if found and updated, or False otherwise.
  • Precondition: library is a list of Song entries, each with a valid rating in [1..5].
  • Error Case: If new_rating is outside [1..5], or song_title is empty, we might raise ValueError.
Now, according to Section 3 of the Design Recipe, we must craft Examples & Tests before or during implementation to clarify and confirm how update_rating should behave. In your answer, do the following:
  1. Provide at least 3 examples of calling update_rating (one typical case, one edge case, and one error/invalid case).
  2. Convert each example into a pytest-style or assert-based test, explaining why you chose these particular inputs and what outcome you expect (True, False, or an exception).
  3. Briefly justify how these tests follow from the data definition and signature (e.g., referencing rating constraints or checking for empty titles).
Write your answer below:
Solution.
Sample Answer (One Possible Approach):
  1. Examples:
    • Typical Case: update_rating(library, "Thriller", 4), where library = [["Imagine", 5, 120], ["Thriller", 2, 100]]. Expect True; rating changes from 2 to 4.
    • Edge Case: update_rating([], "Imagine", 5) on an empty library. Expect False (no songs).
    • Error/Invalid Case: update_rating(library, "Hey Jude", 10), with 10 outside [1..5], expecting ValueError.
  2. Tests (using assert or a test framework):
    def test_update_rating():
        # Typical
        library = [
            ["Imagine", 5, 120],
            ["Thriller", 2, 100]
        ]
        result = update_rating(library, "Thriller", 4)
        assert result == True
        assert library[1][1] == 4  # rating should now be 4
    
        # Edge: empty library
        empty_lib = []
        result2 = update_rating(empty_lib, "Imagine", 5)
        assert result2 == False
    
        # Invalid rating
        try:
            update_rating(library, "Hey Jude", 10)
            assert False, "Expected ValueError for rating=10"
        except ValueError:
            pass  # correct behavior
    
  3. Justification:
    • From the data definition, rating must be in [1..5], so we test an out-of-range rating.
    • The signature says we return bool (True or False) for valid calls, so we confirm typical and edge-case calls produce correct booleans.
    • We include ValueError handling because the signature/purpose states we raise an error if new_rating is invalid.
You have attempted of activities on this page.