COMPUTER PROGRAMMING 1 (PYTHON) • DEBUGGING, TESTING & CODE QUALITY

Writing Test Cases — I can write simple test cases (inputs/expected outputs) for a function and include edge cases.

Learn to verify your code works correctly by designing inputs, predicting outputs, and catching tricky edge cases.

Historical Context & Motivation

Imagine you build a bridge and never test whether it can hold weight before opening it to traffic. That would be reckless, right? The same logic applies to software. Every app on your phone, every website you visit, and every game you play has been tested thousands of times before reaching you. Software testing is the practice of checking that code behaves the way it should, and test cases are the specific checks we design to verify each piece of a program.

The history of testing stretches back to the earliest days of computing. As programs grew more complex, developers realized they needed systematic ways to catch bugs before users encountered them. What started as informal spot-checking evolved into one of the most important disciplines in all of software engineering.

1947
The First "Bug"
Grace Hopper's team found a moth stuck in a relay of the Harvard Mark II computer. They taped it into the logbook, coining the term debugging. This event highlighted the need for systematic error checking in programs.
1957
Early Formal Testing
As languages like FORTRAN emerged, programmers began writing separate routines whose sole purpose was to verify that computational functions returned correct answers for known inputs.
1994
JUnit & Unit Testing Revolution
Kent Beck created the first unit testing framework, SUnit, for Smalltalk. This led to JUnit for Java and eventually pytest for Python—making test-writing simple and accessible for all developers.
2003
Test-Driven Development (TDD)
Kent Beck published his book on Test-Driven Development, popularizing the idea of writing tests before writing the actual code. This approach spread rapidly through the software industry.
2020s
Testing Is Standard Practice
Today, professional codebases often have more lines of test code than production code. Automated test suites run every time new code is submitted, catching bugs before they ever reach users.

This history raises a key question: how do we actually write good test cases? How do we decide what inputs to try, what outputs to expect, and what sneaky scenarios might cause our code to break? That is exactly what this lesson will teach you.

Core Principles of Writing Test Cases

A test case is a specific scenario you design to check whether a function produces the correct output for a given input. Every test case has three key parts: the input you feed to the function, the expected output you predict the function should return, and the actual output the function really returns. If the expected and actual outputs match, the test passes; if they differ, the test fails, and you have found a bug.

1

Input / Expected Output Pairs

Every test case specifies exact inputs to pass to a function and the precise output you expect back. You write these pairs before running the code, based on your understanding of what the function should do.
2

Normal Cases (Happy Path)

Start with typical, expected inputs—the kind of data the function was designed to handle. For a function that adds two numbers, a normal case might be add(3, 5) returning 8.
3

Edge Cases (Boundary Cases)

Edge cases test unusual or extreme inputs: empty strings, zero, negative numbers, very large values, or single-element lists. These are where bugs love to hide because developers often forget to handle them.
4

Assert Statements

In Python, you use assert statements to automate checking. The statement assert add(3, 5) == 8 will silently pass if correct or raise an error if the result is wrong.
5

Test Independence

Each test case should work on its own. One test should not depend on another test running first. This makes it easy to identify exactly which scenario is failing when something breaks.
✦ KEY TAKEAWAY
Think of test cases like a teacher's answer key. Before handing out an exam, the teacher writes down the correct answers. When students turn in their work, the teacher checks each answer against the key. Your test cases are the answer key for your function—you decide what the right answers are first, then check whether your code matches.

How a Test Case Works — Visual Explanation

The diagram below shows the anatomy of a single test case. You provide an input to the function, the function processes it and produces an actual output, and then you compare it against the expected output you predicted. If they match, the test passes; otherwise, the test fails and signals a bug.

The flow of a test case: an input is fed into the function, the actual output is captured, and then compared against the expected output. A match means the test passes.

Notice how the comparison step is separate from the function itself. The function does not know it is being tested. You, as the programmer, write a line of code—typically an assert statement—that performs the comparison. For example, assert add(3, 5) == 8 encodes the entire test case in a single line: the input (3, 5), the function call, and the expected output 8. If the assertion fails, Python raises an AssertionError, immediately alerting you that something is wrong.

How Test Cases Work in Python

Python gives you several ways to write and run test cases. The simplest approach uses the built-in assert statement, which checks whether an expression is True. If the expression is True, nothing happens—the program continues silently. If it is False, Python raises an AssertionError and stops execution so you can investigate the failure.

The Assert Statement Pattern

ASSERT SYNTAX
assert function(input) == expected_output
function(input) — the function call with your chosen test input. expected_output — the value you predict the function should return. == — the comparison operator that checks for equality.

You can also add a custom error message after a comma. For example: assert add(3, 5) == 8, "add(3, 5) should return 8". This message will appear if the assertion fails, making it much easier to identify which test case broke when you have many tests.

Organizing Tests into a Test Function

As your programs grow, you will want to group related test cases inside a test function. By convention, test function names start with test_ so that testing tools like pytest can automatically discover and run them. Here is the general structure:

TEST FUNCTION TEMPLATE
def test_function_name(): # Normal cases assert function(normal_input) == expected # Edge cases assert function(edge_input) == expected
Group normal cases first, then edge cases. Each assert checks one specific scenario.
💡 Running Your Tests
You can run tests in two ways. First, simply call the test function at the bottom of your script: test_function_name(). Second, if you install pytest (pip install pytest), you can run pytest filename.py in your terminal and it will find and run all functions starting with test_ automatically.

Identifying and Classifying Edge Cases

The most important skill in test writing is learning to think about edge cases—unusual or extreme inputs that often reveal hidden bugs. Normal cases prove your code works for typical inputs, but edge cases prove your code is robust. Professional developers often say: "Anyone can write code that works for the happy path. Great programmers write code that handles the weird stuff too."

The five main categories of edge cases. Use this chart as a checklist when writing tests: ask yourself whether your function handles each category.

Let's look at a concrete example. Suppose you write a function find_max(numbers) that returns the largest number in a list. A normal test might be assert find_max([3, 7, 2]) == 7. But what about edge cases? What if the list has only one element? What if all elements are the same? What if the list contains negative numbers? What if the list is empty? Each of these questions becomes a test case, and each might reveal a bug that normal tests miss.

Edge case examples for a find_max function
Edge Case CategoryExample InputExpected OutputWhy It Matters
Single elementfind_max([5])5No comparison needed—does the function still work?
All same valuesfind_max([4, 4, 4])4Ensures function handles ties correctly
Negative numbersfind_max([-3, -1, -7])-1−1 is greater than −3; catches sign confusion
Mixed positive/negativefind_max([-2, 0, 3])3Includes zero—a common source of bugs
Empty listfind_max([])None or ErrorNo elements to compare—function must handle gracefully

Worked Example — Testing a count_vowels Function

Let's walk through the complete process of writing test cases for a function called count_vowels(text) that takes a string and returns the number of vowels (a, e, i, o, u) in it. We will think through normal cases, identify edge cases, and write the full test function in Python.

Writing Tests for count_vowels(text)
1
Step 1 — Understand What the Function DoesThe function count_vowels(text) accepts a string parameter and returns an integer—the total count of vowel characters (a, e, i, o, u) in the string. Before writing tests, you must clearly understand the expected behavior: Should it count uppercase vowels? We'll assume yes, it should be case-insensitive.
Function signature: count_vowels(text: str) → int
2
Step 2 — Write Normal (Happy Path) Test CasesStart with typical inputs that represent everyday usage. A normal word like "hello" contains two vowels (e, o). The word "banana" contains three vowels (a, a, a). Write assert statements for these: assert count_vowels("hello") == 2 assert count_vowels("banana") == 3 assert count_vowels("python") == 1
Three normal cases covering common words
3
Step 3 — Identify Edge Cases Using the ChecklistNow apply the edge case categories from Section 5. Empty: What if the string is ""? Expected: 0. All vowels: What about "aeiou"? Expected: 5. No vowels: What about "rhythm"? Expected: 0. Uppercase: What about "HELLO"? Expected: 2 (case-insensitive). Single character: What about "a" or "b"? Expected: 1 or 0.
Five edge cases identified
4
Step 4 — Write the Edge Case AssertionsConvert each edge case into an assert statement: assert count_vowels("") == 0 # empty string assert count_vowels("aeiou") == 5 # all vowels assert count_vowels("rhythm") == 0 # no vowels assert count_vowels("HELLO") == 2 # uppercase assert count_vowels("a") == 1 # single vowel assert count_vowels("b") == 0 # single consonant
Six edge case assertions added
5
Step 5 — Combine into a Complete Test FunctionWrap everything in a test function and call it: def test_count_vowels(): # Normal cases assert count_vowels("hello") == 2 assert count_vowels("banana") == 3 assert count_vowels("python") == 1 # Edge cases assert count_vowels("") == 0 assert count_vowels("aeiou") == 5 assert count_vowels("rhythm") == 0 assert count_vowels("HELLO") == 2 assert count_vowels("a") == 1 assert count_vowels("b") == 0 print("All tests passed!") test_count_vowels()
Complete test function with 9 test cases covering normal and edge scenarios

Strengths, Limitations, and Best Practices

Writing test cases with assert statements is a powerful habit, but like any tool, it has strengths and limitations. Understanding both will help you use testing effectively and know when to reach for more advanced techniques.

Strengths and limitations of assert-based testing
StrengthsLimitations
Catches bugs early before they reach usersCannot guarantee code is 100% bug-free—only checks cases you thought of
Documents how your function is supposed to behaveAssert stops at the first failure—you don't see all failures at once
Quick to write—just one line per test caseNo built-in reporting—you must add print statements for feedback
Makes refactoring safer—you can change code and rerun testsCannot easily test interactive programs (those using input())
Forces you to think carefully about function behavior before codingTesting frameworks like pytest offer more features (reports, fixtures, etc.)
✦ KEY TAKEAWAY
Testing is like proofreading an essay. You can't catch every possible typo—there are infinitely many things you could write—but checking the important parts (spelling, grammar, thesis) catches most errors. Similarly, you can't test every possible input, but testing normal cases and edge cases catches the vast majority of bugs.

Best Practice Tips

  • Name test functions clearly — Use names like test_add_negative_numbers so you instantly know what is being tested.
  • Test one thing at a time — Each assert should test a single scenario. If it fails, you want to know exactly what went wrong.
  • Add comments labeling each group — Use # Normal cases and # Edge cases comments to organize your test function.
  • Include a success message — Add print("All tests passed!") at the end so you get positive confirmation.
  • Run tests after every change — Whenever you modify your function, rerun all tests to make sure nothing broke.

Connection to Professional Testing

The assert-based testing you're learning here is the foundation for all professional software testing. As you progress in your programming journey, you'll encounter more powerful tools and techniques that build on exactly these same principles. Understanding how your current skills connect to what professionals use will motivate you to keep building your testing habits.

From beginner testing to professional practices
What You're Learning NowProfessional / Advanced Version
assert statementsTesting frameworks like pytest and unittest with detailed reports
Manually running test functionsContinuous Integration (CI) — tests run automatically on every code change
Testing one function at a time (unit tests)Integration tests that check how multiple functions work together
Manually choosing edge casesProperty-based testing (e.g., Hypothesis library) generates thousands of random inputs
Writing tests after writing codeTest-Driven Development (TDD) — write tests first, then write code to make them pass

One of the most exciting next steps is Test-Driven Development (TDD), a methodology where you write your test cases before writing the function itself. You first define what the function should do by writing assertions, then you write the code to make all those assertions pass. This might sound backward, but it forces you to think clearly about what your function should do before you start coding it. Many top software companies use TDD as their standard development practice.

🚀 Looking Ahead
In future courses, you'll learn about pytest fixtures, mocking, code coverage analysis, and how to measure what percentage of your code is actually tested. For now, mastering assert-based testing with edge cases gives you a rock-solid foundation.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain the difference between a normal test case and an edge case. Why are edge cases important to include in your test suite? Give one example of each for a function is_even(n) that returns True if n is an even number.
PROBLEM 2 — BASIC CALCULATION
A function double(x) is supposed to return twice the value of x. Write three assert statements: one normal case, one with zero, and one with a negative number.
PROBLEM 3 — INTERMEDIATE
Write a complete test function test_reverse_string() for a function reverse_string(s) that returns the input string reversed. Include at least two normal cases and three edge cases. Label each group with comments.
PROBLEM 4 — APPLIED
You are building a grade calculator. The function letter_grade(score) takes a numeric score (0–100) and returns a letter grade: 90–100 → "A", 80–89 → "B", 70–79 → "C", 60–69 → "D", below 60 → "F". Write a comprehensive test function that tests boundary values (the exact cutoff scores) and at least two additional edge cases.
PROBLEM 5 — CRITICAL THINKING
A classmate writes the function def count_words(sentence): return len(sentence.split()) and claims it works perfectly. They tested it with assert count_words("I love Python") == 3 and it passed. Identify at least three edge case inputs that could reveal problems with this function. For each, predict what the function would actually return and what the correct output should arguably be. Explain your reasoning.

Lesson Summary

In this lesson, you learned that a test case is a specific check that verifies whether a function produces the correct expected output for a given input. You use Python's assert statement to automate this comparison—writing assert function(input) == expected for each scenario. You organize tests into test functions that start with test_ and group normal cases (typical inputs) and edge cases (empty inputs, zero, negative numbers, boundaries, duplicates, and extreme values).

The key skill is learning to think like a tester: before running your code, ask yourself what could go wrong with empty values, boundary values, negative numbers, and unexpected data types. Testing doesn't guarantee your code is perfect, but it dramatically reduces bugs and builds confidence that your functions work as intended. These skills directly connect to professional practices like Test-Driven Development and automated testing frameworks like pytest that you will encounter in future courses.

Varsity Tutors • Computer Programming 1 (Python) • Writing Test Cases