Computer Programming 1 (Python) • Problem Solving & Mini-Projects

Testing Programs — I can test a program by trying edge cases and fixing UI/logic bugs at my level.

Learn to break your own code on purpose so your users never have to.

A Python coding terminal with a program flow and laptop.
Inside this lesson 10 sections 5 questions 6 figures

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"

Grace Hopper's team logs a real moth found in the Mark II relay, coining the popular use of "bug" and "debugging" in computing.

1970s • Structured Testing Emerges

Glenford Myers publishes The Art of Software Testing, establishing key ideas like boundary analysis and systematic test design.

1999 • Unit Testing Goes Mainstream

Kent Beck releases the JUnit testing framework, sparking the test-driven development (TDD) movement where tests are written before code.

2010s • Continuous Integration & Automation

Automated testing pipelines become standard. Tools like pytest for Python make it easy for even beginners to write and run tests.

Today • Testing as a Core Skill

Industry surveys show that companies consider testing ability a top hiring criterion. AI-assisted testing tools are expanding what one developer can verify.

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.

1

Edge Cases

Edge cases are inputs at the extreme boundaries of what a program should handle — the smallest, largest, emptiest, or most unusual values. If your function accepts ages, edge cases include 0, negative numbers, and extremely large numbers like 999.
2

Normal Cases

Normal cases (also called "happy path" tests) use typical, expected inputs. For an age function, a normal case might be 16 or 35. These verify your code works under ordinary conditions.
3

Logic Bugs

A logic bug occurs when the program runs without crashing but produces the wrong result. For example, using < instead of <= in a comparison, causing an off-by-one error.
4

UI Bugs

A UI bug (user interface bug) means the program's output is confusing, misleading, or incorrectly formatted — even if the underlying calculation is correct. A misspelled label or missing units are common examples.
5

Regression Testing

Regression testing means re-running previous tests after you fix a bug to ensure your fix did not accidentally break something else. Every fix is a potential source of new bugs.
✦

KEY TAKEAWAY

Think of testing like proofreading an essay. You would not only re-read the introduction — you would check the conclusion, every paragraph transition, and especially the tricky sentences where you were unsure of grammar. In the same way, you should test the boundaries and tricky spots in your code, not just the parts you feel confident about. The goal is to find problems before your users do.

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.

Testing Workflow for Python Programs1. Write CodeDraft your function2. Identify Test CasesNormal + Edge cases3. Run TestsExecute & observe4. All tests pass?YES✓NO5. Identify the BugLogic bug or UI bug?6. Fix the BugEdit code carefullyLoop back
The testing workflow forms a loop: write code → identify test cases (normal and edge) → run tests → check results. If a test fails, identify whether the bug is a logic bug or a UI bug, fix it, and loop back to re-test.

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

assertcondition,"errormessage"assert condition, "error message"

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.

Bug Classification & Edge Case MapAll Program BugsSyntax ErrorsLogic BugsUI / Output BugsPython catches thesefor you automaticallyOff-by-One Errorsrange(1, 10)vs range(1, 11)Using < vs <=Wrong Conditionif x > 0 (misses 0)and vs or mix-upInverted booleanFormatting IssuesRounding errorsMissing units / labelsConfusing MessagesUnclear promptsMisspellings in outputCommon edge cases: 0, negative numbers, empty strings "", very large values, special characters
The three main bug categories: syntax errors (caught by Python automatically), logic bugs (wrong results), and UI bugs (confusing output). Edge cases at the bottom are the inputs most likely to reveal logic and UI bugs.
Input TypeExample for a "letter grade" functionWhy It Matters
Normal casegrade(85) → "B"Verifies the basic logic works for typical inputs.
Boundarygrade(90) → "A" or "B"?Tests whether boundary values (90, 80, 70) are handled by ≥ or > comparisons.
Zerograde(0) → "F"Zero is a common edge case that can trigger unexpected behavior.
Negativegrade(-5) → error or "F"?Tests whether the function handles invalid negative scores gracefully.
Over maximumgrade(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. 1

    Step 1 — Test Normal Cases

    Start with typical values. We call letter_grade(95) and get "A" — correct. We call letter_grade(85) and get "B" — correct. We call letter_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. 2

    Step 2 — Test Boundary / Edge Cases

    Now we test the exact boundaries. We call letter_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. 3

    Step 3 — Fix Bug #1 (Logic Bug)

    Change every > to >= in the conditions: if score >= 90:, elif score >= 80:, and so on. Now letter_grade(90) correctly returns "A".
    Logic bug fixed. Boundary tests now pass.
  4. 4

    Step 4 — Fix Bug #2 (UI Bug)

    Change return "you failed" to return "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. 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.

StrategyStrengthsLimitations
Print & CheckEasy 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 StatementsAutomatic 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 FrameworkRuns 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 TestingAnother 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

Choosing a testing strategy is like choosing how to check your math homework. You can re-read your work (print & check), use the answer key on a few problems (asserts), or ask a friend to review it (peer testing). The best approach is usually a combination — start with print statements to explore, then lock in your findings with assert statements so you can easily re-run your tests whenever you change the code.

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 NowProfessional EquivalentKey Difference
Print statements to check outputUnit tests — automated functions that test one piece of codeUnit tests run automatically every time code changes, catching regressions instantly.
Trying edge cases by handBoundary value analysis — formal method for choosing test inputsProfessionals use mathematical models to guarantee they test every boundary.
Asking a friend to test your programQA (Quality Assurance) teams — dedicated testersQA testers follow detailed test plans and use specialized tools to simulate thousands of users.
Re-running tests after a fixCI/CD pipelines — servers that run all tests automaticallyEvery 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

What is the difference between a logic bug and a UI bug? Give one example of each in a program that calculates the area of a rectangle.
Show answer
A logic bug produces an incorrect result — for example, computing area as 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

Consider this function: 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
The bug is that 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

You write a function that checks if a year is a leap year: 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
Three essential edge cases: (1) 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

You are building a simple password checker that prints whether a password is "strong" or "weak." A strong password must be at least 8 characters long and contain at least one digit. Write the function, then write at least four assert statements that test normal cases, edge cases, and potential UI bugs.
Show answer
A sample function: 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

A classmate argues: "I ran my program once and it gave the right answer, so it must be correct." Construct a specific example of a function that gives the correct answer for one typical input but fails for a specific edge case. Use this example to explain why a single successful test is not sufficient evidence that a program is bug-free.
Show answer
Consider a function 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.

Varsity Tutors • Computer Programming 1 (Python) • Testing Programs — I can test a program by trying edge cases and fixing UI/logic bugs at my level.