Module 7.3

Testing

Tests prove your code works and catch bugs before users do. Writing tests might feel like extra work at first, but it saves time in the long run. Tests give you confidence to refactor code, add features, and deploy changes knowing that if something breaks, your tests will catch it immediately.

50 min
Intermediate
Hands-on
What You'll Learn
  • Writing assertions
  • unittest module basics
  • pytest framework
  • Testing edge cases
  • Test organization patterns
Contents
01

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.

Key Concept

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
E2E Tests
Few · Slow · Expensive
Integration Tests
Some · Medium Speed
Unit Tests
Many · Fast · Cheap
UNIT
Test individual functions in isolation
INTEGRATION
Test how components work together
E2E
Test complete user workflows
Write many fast unit tests, fewer integration tests, and even fewer end-to-end tests.

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.

02

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!")
03

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)
04

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

1 What happens when an assertion fails?
2 In unittest, test methods must start with?
3 Which testing framework uses plain assert statements?
4 What type of tests should you have the most of?
5 What does setUp() do in unittest?
6 Which method verifies that an exception is raised?
Answer all questions to check your score