SECTION 1
Historical Context & Motivation
Writing code is only half the battle — the other half is making sure it actually works. Since the dawn of computing, programmers have dealt with bugs, a term popularized when engineers found a literal moth jammed inside a relay of the Harvard Mark II computer in 1947. That small insect became a symbol of a much larger truth: every program will contain errors, and finding them requires a disciplined, intentional process called software testing.
In the early days of programming, testing was informal — programmers would simply run their code and hope for the best. As software grew more complex and the consequences of failure became more severe (think rocket launches, banking systems, and medical devices), the industry developed rigorous testing methods. Today, professional developers spend roughly 30–50% of their total project time on testing. Understanding how to test effectively is one of the most valuable skills you can build, even at the introductory level.
1947 • The First "Bug"
1970s • Structured Testing Emerges
1999 • Unit Testing Goes Mainstream
2010s • Continuous Integration & Automation
Today • Testing as a Core Skill
The central question this lesson addresses is straightforward but powerful: how do you gain confidence that your program actually does what you intended? The answer lies in thinking creatively about inputs, deliberately trying to break your own code, and methodically fixing the problems you uncover.
SECTION 2
Core Principles of Testing
Effective testing is not random button-mashing — it is guided by a set of foundational principles that help you choose which inputs to try and where to look for problems. These principles apply whether you are testing a simple calculator script or a full-fledged web application.
Edge Cases
Normal Cases
Logic Bugs
UI Bugs
Regression Testing
KEY TAKEAWAY
SECTION 3
Visual Explanation — The Testing Workflow
The diagram below illustrates the complete testing workflow you should follow when developing a Python program. It starts with writing code, moves through identifying the kinds of test cases you need, running those tests, and then looping back to fix any bugs you discover. Notice that the process is cyclical — you keep testing and fixing until you are satisfied that the program handles all important scenarios.
As you follow this diagram, notice that steps 2 through 6 may repeat many times for a single program. Professional developers often cycle through this loop dozens of times before releasing software. At your level, even two or three cycles will dramatically improve your code quality. The key insight is that testing is not something you do once at the end — it is woven into the development process from the start.
SECTION 4
How Testing Works in Python
In Python, you can test your code using several strategies. The simplest approach — and the one you should master first — is manual testing with print statements and direct function calls. As you progress, you can adopt assert statements and eventually formal testing frameworks like pytest. This section focuses on the first two approaches since they are immediately useful in your current projects.
Strategy 1: Manual Testing with Print Statements
The most intuitive way to test is to call your function with specific inputs and print the output to see if it matches your expectations. For example, if you write a function is_even(n) that returns True when n is even, you would test it like this:
Manual Test Example
print(is_even(4)) # Expected: True
print(is_even(7)) # Expected: False
print(is_even(0)) # Expected: True (edge case!)
print(is_even(-2)) # Expected: True (edge case!)Strategy 2: Assert Statements
An assert statement is a one-line test built into Python. It checks whether a condition is True. If the condition is False, Python immediately raises an AssertionError, telling you exactly which test failed. The syntax is:
ASSERT SYNTAX
condition — a boolean expression you expect to be True. error message — optional text displayed when the assertion fails, helping you find the problem quickly.
Assert Statement Example
assert is_even(4) == True, "4 should be even"
assert is_even(7) == False, "7 should be odd"
assert is_even(0) == True, "0 should be even"
assert is_even(-3) == False, "-3 should be odd"
If all assertions pass, Python runs silently — silence means success.Choosing Your Test Inputs
You cannot test every possible input (there are infinitely many), so you must be strategic. A good test suite covers three categories of input: normal cases that represent typical usage, edge cases at the boundaries of valid input, and invalid inputs that should be handled gracefully (like entering a word when a number is expected). Even five to ten well-chosen tests can catch the vast majority of bugs in a typical student project.
SECTION 5
Types of Bugs & Edge Cases
Not all bugs are created equal. Understanding the different categories helps you know what to look for and how to fix each type. The diagram below classifies the most common bugs you will encounter in introductory Python programs, along with the kinds of edge cases that expose them.
| Input Type | Example for a "letter grade" function | Why It Matters |
|---|---|---|
| Normal case | grade(85) → "B" | Verifies the basic logic works for typical inputs. |
| Boundary | grade(90) → "A" or "B"? | Tests whether boundary values (90, 80, 70) are handled by ≥ or > comparisons. |
| Zero | grade(0) → "F" | Zero is a common edge case that can trigger unexpected behavior. |
| Negative | grade(-5) → error or "F"? | Tests whether the function handles invalid negative scores gracefully. |
| Over maximum | grade(105) → "A" or error? | Checks for scores above the expected max (like extra credit). |
Test case categories applied to a letter-grade function
SECTION 6
Worked Example — Testing & Fixing a Grade Calculator
Let's walk through a complete example. Suppose you have written a function that converts a numeric score (0–100) into a letter grade. The initial code has two bugs — one logic bug and one UI bug. We will find and fix both using systematic testing.
Original (Buggy) Code
def letter_grade(score):
if score > 90:
return "A"
elif score > 80:
return "B"
elif score > 70:
return "C"
elif score > 60:
return "D"
else:
return "you failed"Testing & Debugging letter_grade()
- 1
Step 1 — Test Normal Cases
Start with typical values. We callletter_grade(95)and get "A" — correct. We callletter_grade(85)and get "B" — correct. We callletter_grade(50)and get "you failed" — the logic may be right but the output is unprofessional.Normal cases mostly pass, but the "F" output is a UI bug — it should say "F", not "you failed". - 2
Step 2 — Test Boundary / Edge Cases
Now we test the exact boundaries. We callletter_grade(90)expecting "A" but we get "B". This reveals a logic bug: the condition uses> 90(strictly greater than) instead of>= 90(greater than or equal to). A student who scores exactly 90 should receive an "A".Bug found:>should be>=in all conditions — this is an off-by-one logic bug. - 3
Step 3 — Fix Bug #1 (Logic Bug)
Change every>to>=in the conditions:if score >= 90:,elif score >= 80:, and so on. Nowletter_grade(90)correctly returns "A".Logic bug fixed. Boundary tests now pass. - 4
Step 4 — Fix Bug #2 (UI Bug)
Changereturn "you failed"toreturn "F"in the else branch. The output should be consistent — all other grades are single letters, so "F" should follow the same pattern.UI bug fixed. Output is now consistent and professional. - 5
Step 5 — Regression Test
Re-run all previous tests:letter_grade(95)→ "A" ✓,letter_grade(90)→ "A" ✓,letter_grade(85)→ "B" ✓,letter_grade(50)→ "F" ✓. All tests pass — the fixes did not introduce new bugs.All tests pass. Debugging complete!
Fixed Code
def letter_grade(score):
if score >= 90: # Fixed: >= instead of >
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F" # Fixed: "F" instead of "you failed"SECTION 7
Comparing Testing Strategies
There are several ways to test your Python code, each with distinct strengths and limitations. As a beginning programmer, you will primarily use manual testing and assert statements, but it helps to understand where these fit in the bigger picture of professional software development.
| Strategy | Strengths | Limitations |
|---|---|---|
| Print & Check | Easy to learn. No setup required. Good for quick checks during development. | You must manually read every output. Easy to miss errors. Not reusable — you have to re-type tests each time. |
| Assert Statements | Automatic pass/fail. Self-documenting. Can be left in code for regression testing. | Stops at the first failure, so you only see one bug at a time. No detailed reporting. |
| pytest Framework | Runs all tests even if some fail. Rich error messages. Industry-standard tool. | Requires installation and learning a new tool. Overkill for very small programs. |
| Peer Testing | Another person may think of inputs you overlooked. Catches UI bugs you are blind to. | Requires coordination. Results depend on the tester's skill and patience. |
Comparison of testing strategies available to introductory Python programmers
KEY TAKEAWAY
SECTION 8
Connection to Professional Testing
The manual and assert-based testing you are learning now is the foundation of everything professionals do — they have simply built more powerful tools on top of these same ideas. Understanding how your current skills connect to professional practices will help you see where your learning journey is headed.
| What You Do Now | Professional Equivalent | Key Difference |
|---|---|---|
| Print statements to check output | Unit tests — automated functions that test one piece of code | Unit tests run automatically every time code changes, catching regressions instantly. |
| Trying edge cases by hand | Boundary value analysis — formal method for choosing test inputs | Professionals use mathematical models to guarantee they test every boundary. |
| Asking a friend to test your program | QA (Quality Assurance) teams — dedicated testers | QA testers follow detailed test plans and use specialized tools to simulate thousands of users. |
| Re-running tests after a fix | CI/CD pipelines — servers that run all tests automatically | Every code change triggers hundreds of automated tests in seconds — no human involved. |
How introductory testing skills map to professional software development practices
As you advance in your programming studies, you will encounter tools like pytest, unittest, and eventually continuous integration platforms like GitHub Actions. For now, the most important skill is the mindset: always ask yourself "what inputs could break this?" before you consider your program finished. That habit will serve you in every programming language and framework you ever use.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
length + width instead of length * width. The program runs without crashing, but the answer is mathematically wrong. A UI bug produces the correct number but presents it poorly — for example, printing "The area is 50" without specifying the unit (square feet, square meters, etc.), or displaying extra decimal places like "50.00000000001" when the user expects a clean result. Both are bugs that need fixing, but they require different troubleshooting approaches.Question 2: BASIC CALCULATION
def discount_price(price, percent_off):
return price - price * percent_off
A user calls discount_price(100, 20) expecting to get 80.0 (a 20% discount on $100) but gets −1900.0 instead. Identify the bug and write the corrected line of code.Show answer
percent_off is being used as a whole number (20) but is being treated as a decimal in the formula. Multiplying 100 × 20 gives 2000, then 100 − 2000 = −1900. The fix is to divide the percentage by 100: return price - price * (percent_off / 100). Now discount_price(100, 20) correctly returns 80.0. This is a classic logic bug where the developer and the user had different assumptions about the input format.Question 3: INTERMEDIATE
def is_leap_year(year):
if year % 4 == 0:
return True
else:
return False
List three edge-case test inputs you should try, explain what the expected output should be for each, and identify whether this function has any bugs for those inputs.Show answer
is_leap_year(2000) — expected True, and the function returns True (correct). (2) is_leap_year(1900) — expected False because years divisible by 100 are NOT leap years unless also divisible by 400. However, the function returns True because it only checks divisibility by 4. This is a logic bug. (3) is_leap_year(0) — the function returns True since 0 % 4 == 0, but the concept of year 0 is debatable. A complete fix requires adding conditions: divisible by 4 AND (not divisible by 100 OR divisible by 400).Question 4: APPLIED
Show answer
def check_password(pw):
has_digit = any(ch.isdigit() for ch in pw)
if len(pw) >= 8 and has_digit:
return "strong"
return "weak"
Four assert statements: (1) assert check_password("hello123!") == "strong" — normal strong password. (2) assert check_password("short1") == "weak" — has a digit but too short (edge case for length). (3) assert check_password("") == "weak" — empty string edge case. (4) assert check_password("abcdefgh") == "weak" — exactly 8 characters but no digit. These tests cover length boundaries, the digit requirement, and the empty-input edge case.Question 5: CRITICAL THINKING
Show answer
def average(nums): return sum(nums) / len(nums). Calling average([10, 20, 30]) returns 20.0 — perfectly correct. But calling average([]) with an empty list causes a ZeroDivisionError because len([]) is 0. One successful test only proves the program works for that one input — it says nothing about the infinite other inputs that could trigger bugs. Edge cases like empty lists, zero, negative numbers, and extremely large values are specifically chosen to probe the boundaries where bugs hide. A thorough test suite gives you much greater confidence in your code's correctness across the full range of realistic inputs.SUMMARY
Lesson Summary
Summary
Testing is the practice of intentionally trying inputs to verify that your program behaves correctly. Effective testing requires you to think beyond normal cases and deliberately explore edge cases — inputs at the boundaries such as zero, negative numbers, empty strings, and values at cutoff thresholds. When a test reveals a problem, you classify it as either a logic bug (wrong result) or a UI bug (confusing or poorly formatted output), then fix it and run regression tests to make sure your fix didn't break anything else.
You can test Python code using print statements for quick exploration and assert statements for reusable, automatic pass/fail checks. The testing mindset — always asking "what input could break this?" — is the single most important habit you can build. It connects directly to professional practices like unit testing and continuous integration, and it will make you a more confident, effective programmer at every stage of your career.