PYTHON • DEBUGGING AND TESTING

Writing Unit Tests — Write simple unit tests using assert statements

Learn to catch bugs before they bite by writing automated checks for your Python functions.

Historical Context & Motivation

Imagine spending hours building a complex program, only to discover a tiny mistake buried deep in your code right before a deadline. This scenario has plagued programmers since the earliest days of computing. In the 1940s, when computers filled entire rooms, engineers had to trace errors by hand through thousands of instructions. As software grew more complex, developers realized they needed a systematic way to verify that each small piece of code worked correctly before assembling the whole program.

The idea of unit testing — checking individual functions or components in isolation — emerged as one of the most powerful strategies for preventing bugs. Rather than waiting until an entire application is built and then searching for errors, unit tests let you verify each building block as you write it. Python's built-in assert statement provides the simplest tool for writing these checks, making it an ideal starting point for learning how to test your code.

1947
The First 'Bug'
Grace Hopper's team found a moth trapped in a relay of the Harvard Mark II computer, coining the term 'debugging.' Engineers began formalizing methods to find and fix errors.
1979
Assertions in C
The C programming language introduced the assert macro, allowing programmers to embed logical checks directly in their code. This idea influenced nearly every language that followed.
1994
First Python Release with Assert
Python 1.0 included the assert statement as a built-in keyword, giving developers a simple, readable way to write inline checks and verify assumptions.
1999
Extreme Programming & TDD
Kent Beck popularized Test-Driven Development (TDD), a practice where developers write tests before writing the actual code. Unit testing became a cornerstone of professional software development.
2004
pytest Emerges
The pytest framework made Python testing even more accessible by using simple assert statements instead of complex test class hierarchies. It remains the most popular Python testing tool today.

So how do you actually write a unit test, and what makes assert so useful? The core question is straightforward: how can you automatically verify that a function produces the correct output for a given input? Let's find out.

Core Principles & Definitions

Before writing your first test, you need to understand a few foundational ideas. Unit testing is built on the principle that you can break any program into small, testable pieces, and then verify each piece independently. Think of it like proofreading each paragraph of an essay one at a time instead of reading the whole thing and hoping you catch every typo.

1

Unit Test

A short piece of code that calls a specific function with known inputs and checks whether the output matches the expected result. Each test targets one small 'unit' of your program.
2

Assert Statement

A Python keyword that evaluates a condition. If the condition is True, nothing happens and the program continues. If it's False, Python raises an AssertionError, signaling a test failure.
3

Expected vs. Actual

Every test compares the expected output (what you know the answer should be) with the actual output (what the function returns). A passing test means these two values match.
4

Test Isolation

Each unit test should be independent — it should not rely on the result of another test. This ensures that when a test fails, you know exactly which function has the bug.
5

Edge Cases

Special or unusual inputs — like zero, negative numbers, empty strings, or very large values — that are most likely to reveal hidden bugs. Good tests always include edge cases.
✦ KEY TAKEAWAY
Think of an assert statement like a teacher's answer key. When you grade a test, you compare the student's answer to the correct one. If they match, the student passes. If they don't, you immediately know something went wrong. The assert statement is your automatic grading machine — it checks answers for you so you don't have to read every output by hand.

Visual Explanation — How Assert Works

The following diagram shows the flow of execution when Python encounters an assert statement. Understanding this flow is critical — it reveals exactly what happens behind the scenes when you run a unit test.

The flowchart shows three stages: (1) the function is called and returns a value, (2) the returned value is compared against the expected value, and (3) execution branches based on whether the condition is True or False. A passing test produces no visible output, while a failing test raises an AssertionError that stops execution immediately.

Notice how a passing test is completely silent — no output, no messages. This is by design. When all your tests pass, you simply see nothing, which means everything is working. The moment something breaks, the AssertionError immediately draws your attention to the exact line where the failure occurred. This "no news is good news" pattern is a hallmark of testing in Python.

How Assert Statements Work

The assert statement follows a simple syntax. You write the keyword assert followed by a condition (any expression that evaluates to True or False). You can optionally add a comma and a message string that appears when the assertion fails, helping you diagnose the problem.

BASIC ASSERT SYNTAX
assert <condition>, <optional message>
If <condition> evaluates to True, execution continues. If False, Python raises AssertionError and prints the optional message.

Assert with Equality Checks

The most common pattern in unit testing is comparing the actual return value of a function against an expected value using ==. Here are the key forms you will use:

EQUALITY ASSERTION
assert function(input) == expected_output
Calls function(input), captures the return value, then checks if it equals expected_output.
ASSERTION WITH MESSAGE
assert function(input) == expected, "descriptive error message"
Adding a message string after the comma makes debugging much easier. When this assertion fails, Python displays your custom message alongside the AssertionError.
BOOLEAN ASSERTION
assert condition_is_true
You can also assert that a condition is True directly, such as assert len(result) > 0 to check that a list is not empty.
⚠️ Important Note
The assert statement can be disabled when Python is run with the -O (optimize) flag. This means you should never use assert for input validation in production code. It is designed specifically for testing and debugging.

Common Testing Patterns & Structure

Professional developers organize their tests using established patterns. Even when using simple assert statements, following a clear structure makes your tests easier to read, maintain, and debug. The most important pattern is called Arrange-Act-Assert (AAA), which divides every test into three distinct phases.

The Arrange-Act-Assert pattern: first set up your test data (Arrange), then call the function (Act), and finally verify the result (Assert). The code example at the bottom shows all three phases in a single test function.

What to Test: Categories of Test Cases

Five categories of test cases every unit test suite should cover
CategoryDescriptionExample for add(a, b)
Normal caseTypical, expected inputsassert add(2, 3) == 5
Edge case: zeroOne or more inputs are zeroassert add(0, 7) == 7
Edge case: negativeNegative numbers as inputsassert add(-1, -4) == -5
BoundaryVery large or very small valuesassert add(10**9, 1) == 10**9 + 1
IdentityInput that should return unchangedassert add(0, 0) == 0

Worked Example — Testing a Grade Calculator

Let's walk through a complete example. Suppose you have written a function called letter_grade(score) that takes a numerical score from 0 to 100 and returns the corresponding letter grade as a string. Here is the function and a full set of unit tests.

Writing Unit Tests for letter_grade()
1
Step 1 — Define the Function to TestFirst, here is the function we want to test. It uses conditional logic to map scores to letter grades: 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 "F"
Function defined with 5 return paths based on score ranges.
2
Step 2 — Test the Normal CasesStart by testing a typical value from each grade range. Pick a value clearly in the middle of each range so there is no ambiguity: assert letter_grade(95) == "A", "95 should be an A" assert letter_grade(85) == "B", "85 should be a B" assert letter_grade(75) == "C", "75 should be a C" assert letter_grade(65) == "D", "65 should be a D" assert letter_grade(50) == "F", "50 should be an F"
All five normal-case assertions pass. ✅
3
Step 3 — Test the Boundary ValuesBoundary values sit right at the edge between two grades. These are the most likely places for off-by-one errors. Test the exact cutoff scores: assert letter_grade(90) == "A", "90 is the boundary for A" assert letter_grade(89) == "B", "89 should be a B, not A" assert letter_grade(80) == "B", "80 is the boundary for B" assert letter_grade(70) == "C", "70 is the boundary for C" assert letter_grade(60) == "D", "60 is the boundary for D" assert letter_grade(59) == "F", "59 should be an F"
Boundary assertions pass — no off-by-one errors. ✅
4
Step 4 — Test Edge CasesCheck extreme values — the lowest and highest possible scores: assert letter_grade(100) == "A", "Perfect score" assert letter_grade(0) == "F", "Zero score"
Edge cases pass. ✅
5
Step 5 — Run All Tests TogetherWrap everything in a test function and call it. If all assertions pass, the function completes silently: def test_letter_grade(): # Normal cases assert letter_grade(95) == "A" assert letter_grade(85) == "B" assert letter_grade(75) == "C" assert letter_grade(65) == "D" assert letter_grade(50) == "F" # Boundaries assert letter_grade(90) == "A" assert letter_grade(89) == "B" assert letter_grade(80) == "B" assert letter_grade(60) == "D" assert letter_grade(59) == "F" # Edge cases assert letter_grade(100) == "A" assert letter_grade(0) == "F" print("All tests passed!") test_letter_grade()
Output: All tests passed! — all 12 assertions verified successfully. 🎉

Assert Statements vs. Print Debugging

You might be wondering: why not just use print() statements to check your results? After all, you can print the output and visually compare it to what you expect. While print() debugging is quick for one-off checks, it falls apart as your program grows. Here is how the two approaches compare.

Comparison of print() debugging vs. assert-based unit testing
Featureprint() Debuggingassert Statements
Automatic?No — you must read each output manuallyYes — fails automatically if wrong
Scales to 100+ tests?Impractical — too much output to readEasily — silent when passing
Catches regressions?Only if you remember to re-checkYes — re-run tests any time
Error message?Whatever you write in the print()Shows line number + custom message
Cleanup needed?Must remove prints before shippingTests live separately from main code
✦ KEY TAKEAWAY
Using print() to debug is like checking your side mirrors every few seconds while driving — it works but requires constant attention. Using assert is like having a dashboard warning light: it stays off when everything is fine and only alerts you the moment something goes wrong. As your programs get bigger, you need the warning light approach.

Connection to Professional Testing Frameworks

The assert statement is the foundation, but professional developers use testing frameworks that build on this same concept. These frameworks provide features like automatic test discovery, detailed failure reports, setup/teardown routines, and the ability to run hundreds of tests with a single command. The two most popular frameworks in Python are unittest (built into Python) and pytest (a third-party library). The good news? The assert syntax you are learning now is exactly what pytest uses.

Progression from simple assert to professional testing frameworks
FeaturePlain assertunittest (built-in)pytest (advanced)
Syntaxassert x == yself.assertEqual(x, y)assert x == y
Learning curveMinimal — one keywordModerate — requires class structureLow — plain functions with assert
Auto-discoveryNo — must call manuallyYesYes
Failure detailLine number + messageExpected vs. actual valuesRich diff with variable introspection
Best forQuick checks, learningLarger projects, Java-style teamsMost Python projects

As you continue learning, you will likely move from plain assert statements to pytest, which uses the exact same syntax but adds powerful features on top. Think of learning assert as learning to drive in a parking lot — you are mastering the fundamental controls that you will use on every road and highway later.

Practice Problems

PROBLEM 1 — CONCEPTUAL
What happens when Python executes assert 2 + 2 == 5? What happens when it executes assert 2 + 2 == 4? Explain the difference in behavior.
PROBLEM 2 — BASIC CALCULATION
Given the function below, write three assert statements to test it — one normal case, one with zero, and one with a negative number. def double(n): return n * 2
PROBLEM 3 — INTERMEDIATE
The function below is supposed to return the maximum value from a list, but it contains a bug. Write assert statements that would catch the bug, and identify the problem. def find_max(numbers): max_val = 0 for num in numbers: if num > max_val: max_val = num return max_val
PROBLEM 4 — APPLIED
You are building a simple password validator. Write the function is_valid_password(password) that returns True if the password is at least 8 characters long and contains at least one digit, and False otherwise. Then write at least four assert statements testing different scenarios including edge cases.
PROBLEM 5 — CRITICAL THINKING
A classmate argues: 'I don't need to write tests. I just run my program and look at the output to see if it's right.' Write a response explaining at least three specific scenarios where this approach fails and assert-based unit testing would have caught the problem. Use concrete examples from a project with multiple functions.

Summary

Unit tests are small, focused checks that verify individual functions produce correct outputs for known inputs. Python's built-in assert statement is the simplest tool for writing these tests: it evaluates a condition and raises an AssertionError if the condition is False, while passing silently when the condition is True. The basic syntax is assert function(input) == expected_output, "error message".

Every well-structured test follows the Arrange-Act-Assert pattern: set up inputs, call the function, and check the result. Thorough test suites include normal cases, boundary values, and edge cases like zero, negative numbers, and empty inputs. Mastering assert prepares you for professional frameworks like pytest, which uses the same syntax to power industry-grade testing workflows.

Varsity Tutors • Python • Writing Unit Tests — Write simple unit tests using assert statements