Why Test?
Testing is not about proving code works perfectly. It is about catching bugs early, documenting expected behavior, and enabling safe refactoring. Good tests act as a safety net that lets you make changes with confidence.
Tests Are Executable Documentation
Tests show how your code should be used and what output to expect. Unlike comments, tests cannot become outdated because they fail when behavior changes. They serve as living documentation.
Why it matters: Future developers (including future you) can read tests to understand how functions work without digging through implementation details.
The Testing Pyramid
Benefits of Testing
Catch Bugs Early
Find issues during development when they are cheap to fix, not in production where they cost 10x more.
Enable Refactoring
Change code structure confidently knowing tests will catch regressions immediately.
Document Behavior
Tests show expected inputs and outputs, serving as living documentation that never goes stale.
Deploy with Confidence
Run tests before deployment to ensure nothing is broken and ship quality code.
Assertions
Assertions are the building blocks of tests. An assertion checks if a condition is true. If not, it raises an error. Python's built-in assert statement is the simplest form, while testing frameworks provide richer assertion methods.
Basic Assert Statement
# Simple assertions
assert 2 + 2 == 4 # Passes silently
assert "hello".upper() == "HELLO" # Passes
# With error message
x = 10
assert x > 0, "x must be positive"
# Assertion that fails
assert 5 > 10 # AssertionError!
Assert passes silently when the condition is true. When false, it raises AssertionError with your optional message.
Testing Functions with Assert
def add(a, b):
return a + b
def square(n):
return n * n
# Test our functions
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
assert square(4) == 16
assert square(-3) == 9
assert square(0) == 0
print("All tests passed!")
Test multiple inputs including zero, negative numbers, and edge cases. If any assertion fails, the script stops immediately.
Practice: Assertions
Task: Write a get_max(a, b) function and test it with at least 3 assertions.
Show Solution
def get_max(a, b):
return a if a > b else b
# Tests
assert get_max(5, 3) == 5
assert get_max(1, 10) == 10
assert get_max(7, 7) == 7 # Equal values
assert get_max(-5, -2) == -2 # Negatives
print("All tests passed!")
Task: Write reverse_string(s) and test with normal strings, empty string, and single char.
Show Solution
def reverse_string(s):
return s[::-1]
# Tests
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
assert reverse_string("ab") == "ba"
print("All tests passed!")
Task: Write is_palindrome(s) and test with palindromes, non-palindromes, empty, and single char.
Show Solution
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
assert is_palindrome("radar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A") == True
assert is_palindrome("A man a plan a canal Panama") == True
print("All tests passed!")
The unittest Module
Python's built-in unittest module provides a structured way to write and organize tests. You create test classes that inherit from TestCase, and each test method name starts with "test_". The module provides many assertion methods beyond basic assert.
Basic unittest Structure
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
def test_zero(self):
self.assertEqual(add(0, 0), 0)
if __name__ == "__main__":
unittest.main()
Each test method is independent. The test runner finds all methods starting with "test_" and runs them, reporting pass/fail for each.
Common Assertion Methods
| Method | Checks That |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIn(a, b) | a in b |
assertIsNone(x) | x is None |
assertRaises(exc) | Exception is raised |
Testing Exceptions
class TestDivide(unittest.TestCase):
def test_divide_by_zero(self):
with self.assertRaises(ZeroDivisionError):
result = 10 / 0
def test_invalid_type(self):
with self.assertRaises(TypeError):
result = "10" / 2
Use assertRaises as a context manager to verify that code raises the expected exception. The test passes if the exception is raised.
unittest
Built-in- Built into Python
- No installation needed
- Class-based structure
- Verbose syntax
- self.assertX methods
pytest
Recommended- Simple assert statements
- No boilerplate needed
- Better error messages
- Rich plugin ecosystem
- Requires pip install
Practice: unittest
Task: Write a TestCalculator class with tests for add() and subtract() methods.
Show Solution
import unittest
class Calculator:
def add(self, a, b): return a + b
def subtract(self, a, b): return a - b
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)
def test_subtract(self):
self.assertEqual(self.calc.subtract(5, 3), 2)
Task: Write a test that verifies divide(a, b) raises ValueError when b is 0.
Show Solution
import unittest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_divide_normal(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
Task: Write a test class for a Stack with setUp creating a fresh stack and tests for push, pop, and is_empty.
Show Solution
import unittest
class Stack:
def __init__(self): self.items = []
def push(self, x): self.items.append(x)
def pop(self): return self.items.pop()
def is_empty(self): return len(self.items) == 0
class TestStack(unittest.TestCase):
def setUp(self):
self.stack = Stack()
def test_is_empty_initially(self):
self.assertTrue(self.stack.is_empty())
def test_push_and_pop(self):
self.stack.push(42)
self.assertEqual(self.stack.pop(), 42)
pytest Framework
pytest is the most popular Python testing framework. It uses simple assert statements, has minimal boilerplate, provides excellent error messages, and has a rich plugin ecosystem. Most Python developers prefer pytest over unittest.
Basic pytest Tests
# test_math.py - pytest uses simple functions
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 0) == 0
# Run with: pytest test_math.py
No class or inheritance needed. Just write functions starting with "test_" and use plain assert. pytest discovers and runs them automatically.
Testing Exceptions with pytest
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
def test_divide_normal():
assert divide(10, 2) == 5
Use pytest.raises() as a context manager. It works similarly to unittest's assertRaises but with cleaner syntax.
Testing Edge Cases
def test_empty_list():
assert sum([]) == 0
def test_single_element():
assert sum([5]) == 5
def test_negative_numbers():
assert sum([-1, -2, -3]) == -6
def test_mixed_numbers():
assert sum([-1, 0, 1]) == 0
Always test edge cases: empty inputs, single elements, negative numbers, zeros, and boundary values. Bugs often hide in edge cases.
Practice: pytest
Task: Write tests for greet(name) that returns "Hello, {name}!". Test with different names.
Show Solution
def greet(name):
return f"Hello, {name}!"
def test_greet_alice():
assert greet("Alice") == "Hello, Alice!"
def test_greet_empty():
assert greet("") == "Hello, !"
def test_greet_numbers():
assert greet("123") == "Hello, 123!"
Task: Write get_positives(nums) and test with mixed, all positive, all negative, and empty lists.
Show Solution
def get_positives(nums):
return [n for n in nums if n > 0]
def test_mixed():
assert get_positives([-1, 2, -3, 4]) == [2, 4]
def test_all_positive():
assert get_positives([1, 2, 3]) == [1, 2, 3]
def test_all_negative():
assert get_positives([-1, -2]) == []
def test_empty():
assert get_positives([]) == []
Task: Write fizzbuzz(n) returning "Fizz", "Buzz", "FizzBuzz", or the number. Test all cases.
Show Solution
def fizzbuzz(n):
if n % 15 == 0: return "FizzBuzz"
if n % 3 == 0: return "Fizz"
if n % 5 == 0: return "Buzz"
return str(n)
def test_fizz():
assert fizzbuzz(3) == "Fizz"
assert fizzbuzz(9) == "Fizz"
def test_buzz():
assert fizzbuzz(5) == "Buzz"
def test_fizzbuzz():
assert fizzbuzz(15) == "FizzBuzz"
def test_number():
assert fizzbuzz(7) == "7"
Key Takeaways
Tests Are Essential
Tests catch bugs early, enable safe refactoring, and document expected behavior.
Testing Pyramid
Write many fast unit tests, fewer integration tests, and few end-to-end tests.
Test Edge Cases
Always test empty inputs, single elements, negatives, zeros, and boundary values.
unittest Is Built-in
Use unittest when you cannot install packages. It is always available.
pytest Is Preferred
Most developers prefer pytest for its simple syntax and better error messages.
Tests as Documentation
Tests show how to use your code correctly. They are living documentation.
Knowledge Check
Quick Quiz
Test what you've learned about testing in Python