Unit Testing#

⚠️ Warning: I used GenAI to assist in writing this lecture (and have never done so previously). While I moderately (heavily?) edited what was produced, I'm going to ask your feedback at the end, and I'd love your honest thoughts.

Contents#

Course Announcements

Due this week:

  • A5 due Sunday (Scientific Computing)

Notes:

  • CL8 due next Friday (Testing & Documentation)

    • labs will happen both weeks, but nothing to turn in this week

    • go this week to get Code Testing and/or E2 questions answered

  • Please complete SETs

E2 Summary

  • Scores now posted on Canvas

    • points will match with what you see on PL

  • Class Median (w/ Curve): 78%

    • Perfect scores: 6 students (w/o curve; 1%); 99 (w/ curve; 17%)

    • Class Median (w/o curve) dropped from 82->58% from Mon-Fri

  • Practice Exam + E2-Review notes/videos on course website likely helpful if you didn’t already use them for studying

11AM Notes:

  • did well on practice; found real E2 harder

    • surprised by difficulty of the last few questions

    • No checkbox questions on practice exam; these are stressful

    • Would have been helpful to have E2-Review questions on the practice exam

    • Actual exam felt heavier on lists and dictionaries; actually felt balance was less classes….heavier on debugging; felt unprepared b/c practice exams not reflective

  • Classes were a big topic; didn’t feel confident on them (more time before exam + more practice helpful)

  • On MC: Tripped up on amount of vocab (class attribute, instance attribute, etc.) and wording to parse

2PM

  • FITB: more of this on practice would have been helpful

  • Content is logic heavy; took longer to think - was not ideally balance +1 to needing more time, specifically to wrap head around what debugging was asking…and then to do it

  • Car() - ‘Ferrari’; Ferrari

  • adding to a dictionary - blanked/needed more practice on this

  • point distribution; got most of it, but not the last part…but didn’t get points that felt reflective of what you knew/did correct

  • Have practice exams timed: (if students have extra time, this is problematic)


What is a Unit Test?#

A unit test checks that one small, isolated unit of your code (usually a single function) behaves the way you expect it to.

Key ideas:

  • Tests are code that calls your code and checks the output

  • If the output matches what you expected → the test passes

  • If the output does not match → the test fails

Taste Test Analogy: you follow the recipe (your function), then taste the result (your test) to see if it came out right.

Why Test?#

  • Catch bugs early - before they wreak havoc

  • Understand your own code - forced to think carefully about what your function should do

  • Safe refactoring — change later; know if problematic immediately

  • Professional practice — testing is expected IRL


Recall: assertstatements#

The simplest way to test in Python is the assert statement.

Syntax:

assert <expression>, "Optional error message if this fails"
  • If <expression> is True → nothing happens (the test “passes silently”)

  • If <expression> is False → Python raises an AssertionError

# a passing assert - nothing is printed, no error raised
assert 2 + 2 == 4
# a failing assert - raises an AssertionError
assert 2 + 2 == 5
# a failing assert with a helpful message
assert 2 + 2 == 5, "2 + 2 does not equal 5!"

Testing a Function with assert#

Notes:

  • This is what we’ve been doing in all your assignment notebooks

  • ‼️ This is NOT yet a unit test…we’re getting there!

# define a function we want to test
def add(a, b):
    """Return the sum of a and b."""
    return a + b
# test: two positive integers
assert add(2, 3) == 5

# test: a negative number
assert add(-1, 1) == 0

# test: floats
assert add(0.1, 0.2) == 0.30000000000000004   # floating-point quirk!

# test: zero
assert add(0, 0) == 0

print("All tests passed!")

Notice the float test above — floating-point arithmetic in Python does not behave the way you might expect from math class. Testing helps you discover these surprises.

0.1 + 0.2  # → 0.30000000000000004  (not 0.3!)
# convince ourselves of float weirdness 
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.10 + 0.20)

What Makes a Good Unit Test?#

Not all tests are created equal. A great test suite is just as important as great production code.

Here are the key properties of a good unit test:

1. Tests exactly ONE thing#

Each test should have a single, clear purpose. If a test fails, you should immediately know what broke.

Bad — one test checking many unrelated things:

# bad: checking multiple unrelated behaviors in one block
# if this fails, which part failed?
def test_everything():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert type(add(1, 2)) == int
    assert len("hello") == 5       # doesn't even test add()!
    assert add(100, 200) == 300
test_everything()

Good — each test has one job:

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-1, -2) == -3

def test_add_returns_correct_type():
    assert type(add(1, 2)) == int
test_add_positive_numbers()
test_add_negative_numbers()
test_add_returns_correct_type()

2. Has a descriptive name#

The test name should read like a sentence describing what behavior is being checked. When a test fails, you read its name first.

❌ Avoid

✅ Prefer

test_1()

test_add_returns_zero_when_inputs_cancel()

test_func()

test_is_palindrome_ignores_spaces()

my_test()

test_count_vowels_on_empty_string()

3. Covers edge cases#

Most bugs hide in edge cases — the unusual inputs you didn’t think about when writing the function.

Common edge cases to always think about:

  • Empty input: empty string "", empty list [], zero 0

  • Negative numbers: does your function handle them?

  • One element: list with a single item

  • Large values: what happens with a very big number?

  • Wrong type: what if someone passes a number where a string is expected?

def count_words(sentence):
    """Return the number of words in a sentence."""
    return len(sentence.split())
# normal case
assert count_words("hello world") == 2

# edge case: single word
assert count_words("hello") == 1

# edge case: empty string
assert count_words("") == 0

# edge case: space at end of word
assert count_words("hello ") == 1

# edge case: space at beginning of word
assert count_words(" hello") == 1

# edge case: lots of spaces between words
assert count_words("hello   world") == 2   # .split() handles this correctly!

# edge case: punctuation included
assert count_words("hello world!") == 2

print("All edge case tests passed!")

4. Is independent#

Tests should NOT depend on each other.

  • Running them in a different order should give the same result

  • Output from one test should not be needed for another


Testing with unittest#

Writing raw assert statements works, but Python’s built-in unittest module gives us a much more organized, scalable way to write tests.

With unittest:

  • Tests are grouped into classes that inherit from unittest.TestCase

  • class name starts with Test…and then includes what it’s testing

  • Each test is a method that starts with test_

  • You get helpful assertion methods (more readable than plain assert)

  • You get a clear summary of which tests passed and which failed

Note: You will be required to use unittest on both the final project and final exam.

Basic Structure#

import unittest

# step 1: define a class that inherits from unittest.TestCase
class TestAdd(unittest.TestCase):

    # step 2: each test is a method starting with test_
    def test_add_two_positives(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_negative_and_positive(self):
        self.assertEqual(add(-1, 1), 0)

    def test_add_two_negatives(self):
        self.assertEqual(add(-3, -7), -10)

    def test_add_zeros(self):
        self.assertEqual(add(0, 0), 0)

Running unittest inside a Jupyter Notebook#

Normally we’re running tests from an external file (coming up next!) but you can run it on a test defined in a notebook:

# run the test class defined above
# verbosity=2 gives detailed output for each test
unittest.main(argv=[''], verbosity=2, exit=False);
test_add_negative_and_positive (__main__.TestAdd.test_add_negative_and_positive) ... ok
test_add_two_negatives (__main__.TestAdd.test_add_two_negatives) ... ok
test_add_two_positives (__main__.TestAdd.test_add_two_positives) ... ok
test_add_zeros (__main__.TestAdd.test_add_zeros) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.006s

OK

Reading the output:

  • ok = test passed

  • FAIL = test ran but the assertion was wrong

  • ERROR = test crashed with an unexpected exception

unittest Assertion Methods#

Instead of raw assert, unittest.TestCase gives you purpose-built methods. These produce much better error messages when a test fails.

Method

What it checks

self.assertEqual(a, b)

a == b

self.assertNotEqual(a, b)

a != b

self.assertTrue(x)

bool(x) is True

self.assertFalse(x)

bool(x) is False

self.assertIsNone(x)

x is None

self.assertIn(a, b)

a in b

self.assertRaises(Error, func, args)

func(args) raises Error

self.assertAlmostEqual(a, b)

a b (useful for floats!)

def divide(num1, num2):
    return num1/num2
# examples of each of the above
class TestAssertionMethods(unittest.TestCase):

    def test_equal(self):
        self.assertEqual(1 + 1, 2)

    def test_not_equal(self):
        self.assertNotEqual("hello", "world")

    def test_true(self):
        self.assertTrue(5 > 3)

    def test_false(self):
        self.assertFalse(3 > 5)

    def test_in(self):
        self.assertIn("cat", ["dog", "cat", "fish"])

    def test_almost_equal(self):
        # perfect for floats — checks within 7 decimal places by default
        self.assertAlmostEqual(0.1 + 0.2, 0.3)

    def test_raises(self):
        # check that divide() raises ZeroDivisionError when b is 0
        self.assertRaises(ZeroDivisionError, divide, 10, 0)

unittest.main(argv=[''], verbosity=2, exit=False)
test_add_negative_and_positive (__main__.TestAdd.test_add_negative_and_positive) ... ok
test_add_two_negatives (__main__.TestAdd.test_add_two_negatives) ... ok
test_add_two_positives (__main__.TestAdd.test_add_two_positives) ... ok
test_add_zeros (__main__.TestAdd.test_add_zeros) ... ok
test_almost_equal (__main__.TestAssertionMethods.test_almost_equal) ... ok
test_equal (__main__.TestAssertionMethods.test_equal) ... ok
test_false (__main__.TestAssertionMethods.test_false) ... ok
test_in (__main__.TestAssertionMethods.test_in) ... ok
test_not_equal (__main__.TestAssertionMethods.test_not_equal) ... ok
test_raises (__main__.TestAssertionMethods.test_raises) ... ok
test_true (__main__.TestAssertionMethods.test_true) ... ok
test_empty_list_raises_error (__main__.TestGetLastElement.test_empty_list_raises_error) ... ok
test_output_empty_list (__main__.TestGetLastElement.test_output_empty_list) ... ok
test_output_integers (__main__.TestGetLastElement.test_output_integers) ... ok
test_output_strings (__main__.TestGetLastElement.test_output_strings) ... ok
test_this_will_fail (__main__.TestIntentionalFailure.test_this_will_fail) ... FAIL
test_add_item_increases_count (__main__.TestShoppingCart.test_add_item_increases_count) ... ok
test_add_multiple_items (__main__.TestShoppingCart.test_add_multiple_items) ... ok
test_cart_contains_added_item (__main__.TestShoppingCart.test_cart_contains_added_item) ... ok
test_new_cart_is_empty (__main__.TestShoppingCart.test_new_cart_is_empty) ... ok
test_remove_item_decreases_count (__main__.TestShoppingCart.test_remove_item_decreases_count) ... ok

======================================================================
FAIL: test_this_will_fail (__main__.TestIntentionalFailure.test_this_will_fail)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/767755369.py", line 5, in test_this_will_fail
    self.assertEqual(string_utils.reverse_string("hello"), "hello")
AssertionError: 'olleh' != 'hello'
- olleh
+ hello


----------------------------------------------------------------------
Ran 21 tests in 0.016s

FAILED (failures=1)
<unittest.main.TestProgram at 0x105b5da10>

Activity: Comprehension Check#

Complete this Google Form: https://forms.gle/W9fHHHv5S7fZNZSh9

setUp and tearDown#

If multiple tests need the same setup (e.g., creating an object to test), use setUp(). It runs automatically before each test method.

tearDown() runs after each test and is useful for cleanup.

class ShoppingCart:
    """A very simple shopping cart."""

    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def remove_item(self, item):
        self.items.remove(item)

    def total_items(self):
        return len(self.items)

    def is_empty(self):
        return len(self.items) == 0
class TestShoppingCart(unittest.TestCase):

    def setUp(self):
        # this runs before EVERY test method below
        # each test gets a fresh cart — they can't interfere with each other!
        self.cart = ShoppingCart()

    def test_new_cart_is_empty(self):
        self.assertTrue(self.cart.is_empty())

    def test_add_item_increases_count(self):
        self.cart.add_item("apple")
d
    def test_add_multiple_items(self):
        self.cart.add_item("apple")
        self.cart.add_item("banana")
        self.assertEqual(self.cart.total_items(), 2)

    def test_remove_item_decreases_count(self):
        self.cart.add_item("apple")
        self.cart.remove_item("apple")
        self.assertTrue(self.cart.is_empty())

    def test_cart_contains_added_item(self):
        self.cart.add_item("mango")
        self.assertIn("mango", self.cart.items)

unittest.main(argv=[''], verbosity=2, exit=False)
test_add_negative_and_positive (__main__.TestAdd.test_add_negative_and_positive) ... ok
test_add_two_negatives (__main__.TestAdd.test_add_two_negatives) ... ok
test_add_two_positives (__main__.TestAdd.test_add_two_positives) ... ok
test_add_zeros (__main__.TestAdd.test_add_zeros) ... ok
test_almost_equal (__main__.TestAssertionMethods.test_almost_equal) ... ok
test_equal (__main__.TestAssertionMethods.test_equal) ... ok
test_false (__main__.TestAssertionMethods.test_false) ... ok
test_in (__main__.TestAssertionMethods.test_in) ... ok
test_not_equal (__main__.TestAssertionMethods.test_not_equal) ... ok
test_raises (__main__.TestAssertionMethods.test_raises) ... ERROR
test_true (__main__.TestAssertionMethods.test_true) ... ok
test_add_item_increases_count (__main__.TestShoppingCart.test_add_item_increases_count) ... ok
test_add_multiple_items (__main__.TestShoppingCart.test_add_multiple_items) ... ok
test_cart_contains_added_item (__main__.TestShoppingCart.test_cart_contains_added_item) ... ok
test_new_cart_is_empty (__main__.TestShoppingCart.test_new_cart_is_empty) ... ok
test_remove_item_decreases_count (__main__.TestShoppingCart.test_remove_item_decreases_count) ... ok

======================================================================
ERROR: test_raises (__main__.TestAssertionMethods.test_raises)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/4273677520.py", line 25, in test_raises
    self.assertRaises(ValueError, divide, 10, 0)
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 766, in assertRaises
    return context.handle('assertRaises', args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 237, in handle
    callable_obj(*args, **kwargs)
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/2956303247.py", line 2, in divide
    return num1/num2
           ~~~~^~~~~
ZeroDivisionError: division by zero

----------------------------------------------------------------------
Ran 16 tests in 0.014s

FAILED (errors=1)
<unittest.main.TestProgram at 0x1052e4110>

Notice: because setUp creates a fresh self.cart before each test, none of these tests interfere with each other — even though they all share the same class.

Activity: Writing unittest Tests#

Below is a function with a subtle bug. Your job:

  1. Write at least 3 unit test methods using unittest.TestCase that would catch the bug

  2. Run your tests — do any fail?

  3. Fix the bug in the function and confirm all tests pass

In the Google form include your Test class code: https://forms.gle/vajdCT4DT2gRnQzDA

def get_last_element(lst):
    """Return the last element of a list.
    
    Should raise IndexError on an empty list.
    """
    # is there a bug here? write tests and find out!
    return lst[-1]
get_last_element([1, 2, 3])
3
get_last_element([])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[16], line 1
----> 1 get_last_element([])

Cell In[13], line 7, in get_last_element(lst)
      2 """Return the last element of a list.
      3 
      4 Should raise IndexError on an empty list.
      5 """
      6 # is there a bug here? write tests and find out!
----> 7 return lst[-1]

IndexError: list index out of range
class TestGetLastElement(unittest.TestCase):
    
    def test_output_integers(self):
        self.assertEqual(get_last_element([1, 2, 3]), 3)
        self.assertEqual(get_last_element([1, 2]), 2)

    def test_output_strings(self):
        self.assertEqual(get_last_element(['a', 'b', 'c', 'd']), 'd')

    def test_output_empty_list(self):
        # self.assertEqual(get_last_element([]), [])
        self.assertRaises(IndexError, get_last_element, [])

    def test_empty_list_raises_error(self):
        with self.assertRaises(IndexError):
            get_last_element([])

unittest.main(argv=[''], verbosity=2, exit=False)
test_add_negative_and_positive (__main__.TestAdd.test_add_negative_and_positive) ... ok
test_add_two_negatives (__main__.TestAdd.test_add_two_negatives) ... ok
test_add_two_positives (__main__.TestAdd.test_add_two_positives) ... ok
test_add_zeros (__main__.TestAdd.test_add_zeros) ... ok
test_almost_equal (__main__.TestAssertionMethods.test_almost_equal) ... ok
test_equal (__main__.TestAssertionMethods.test_equal) ... ok
test_false (__main__.TestAssertionMethods.test_false) ... ok
test_in (__main__.TestAssertionMethods.test_in) ... ok
test_not_equal (__main__.TestAssertionMethods.test_not_equal) ... ok
test_raises (__main__.TestAssertionMethods.test_raises) ... ERROR
test_true (__main__.TestAssertionMethods.test_true) ... ok
test_empty_list_raises_error (__main__.TestGetLastElement.test_empty_list_raises_error) ... ok
test_output_empty_list (__main__.TestGetLastElement.test_output_empty_list) ... ok
test_output_integers (__main__.TestGetLastElement.test_output_integers) ... ok
test_output_strings (__main__.TestGetLastElement.test_output_strings) ... ok
test_add_item_increases_count (__main__.TestShoppingCart.test_add_item_increases_count) ... ok
test_add_multiple_items (__main__.TestShoppingCart.test_add_multiple_items) ... ok
test_cart_contains_added_item (__main__.TestShoppingCart.test_cart_contains_added_item) ... ok
test_new_cart_is_empty (__main__.TestShoppingCart.test_new_cart_is_empty) ... ok
test_remove_item_decreases_count (__main__.TestShoppingCart.test_remove_item_decreases_count) ... ok

======================================================================
ERROR: test_raises (__main__.TestAssertionMethods.test_raises)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/4273677520.py", line 25, in test_raises
    self.assertRaises(ValueError, divide, 10, 0)
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 766, in assertRaises
    return context.handle('assertRaises', args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 237, in handle
    callable_obj(*args, **kwargs)
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/2956303247.py", line 2, in divide
    return num1/num2
           ~~~~^~~~~
ZeroDivisionError: division by zero

----------------------------------------------------------------------
Ran 20 tests in 0.012s

FAILED (errors=1)
<unittest.main.TestProgram at 0x1053057d0>

Reminder of common edge cases to consider:

  • Empty input: empty string "", empty list [], zero 0

  • Negative numbers: does your function handle them?

  • One element: list with a single item

  • Large values: what happens with a very big number?

  • Wrong type: what if someone passes a number where a string is expected?


Running Tests on an External Module#

In a real project, your functions live in a module file (a .py file), not in your notebook. Your test suite should import and test that module.

This is the (suggested) setup you’ll use for your final project:

my_project/
├── my_notebook.ipynb       ← runs everything, including tests
├── string_utils.py         ← your module (code lives here)
└── test_string_utils.py    ← your test file

The Module: string_utils.py#

Module file provided: string_utils.py. It has been saved in the same folder as this notebook.

It contains four functions:

Function

What it does

reverse_string(s)

Returns the string reversed

count_vowels(s)

Counts vowels (a, e, i, o, u)

is_palindrome(s)

Checks if a string reads the same forwards and backwards

capitalize_words(s)

Capitalizes the first letter of each word

Step 1: Import the Module#

# import the external module — just like any other import
import string_utils
# explore what's available in the module
dir(string_utils)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'capitalize_words',
 'count_vowels',
 'is_palindrome',
 'reverse_string']
# check the docstring for a function
string_utils.reverse_string?
Signature: string_utils.reverse_string(s)
Docstring:
Return the reverse of a string.

Parameters
----------
s : str
    The string to reverse.

Returns
-------
str
    The reversed string.

Examples
--------
>>> reverse_string("hello")
'olleh'
File:      ~/Desktop/Teaching/COGS18/LectureNotes-COGS18/string_utils.py
Type:      function

Step 2: Try the Functions Interactively#

Before writing tests, it’s often helpful to call the functions yourself to understand what they return.

# try each function
print(string_utils.reverse_string("hello"))
olleh
print(string_utils.count_vowels("Hello World"))
3
print(string_utils.is_palindrome("racecar"))
print(string_utils.is_palindrome("A man a plan a canal Panama"))
True
True
print(string_utils.capitalize_words("hello world from cogs 18"))
Hello World From Cogs 18

Step 3: Write Tests for the External Module#

Now we write a full unittest test class that imports from string_utils and tests each function.

import unittest
import string_utils

class TestReverseString(unittest.TestCase):

    def test_basic_word(self):
        self.assertEqual(string_utils.reverse_string("hello"), "olleh")

    def test_single_character(self):
        self.assertEqual(string_utils.reverse_string("a"), "a")

    def test_empty_string(self):
        self.assertEqual(string_utils.reverse_string(""), "")

    def test_palindrome_unchanged(self):
        # reversing a palindrome should give back the same string
        self.assertEqual(string_utils.reverse_string("racecar"), "racecar")

    def test_raises_on_non_string(self):
        # passing a non-string should raise TypeError
        self.assertRaises(TypeError, string_utils.reverse_string, 42)


class TestCountVowels(unittest.TestCase):

    def test_basic_word(self):
        self.assertEqual(string_utils.count_vowels("hello"), 2)

    def test_case_insensitive(self):
        # 'A' and 'a' should both count
        self.assertEqual(string_utils.count_vowels("AEIOU"), 5)

    def test_no_vowels(self):
        self.assertEqual(string_utils.count_vowels("gym"), 0)

    def test_empty_string(self):
        self.assertEqual(string_utils.count_vowels(""), 0)

    def test_raises_on_non_string(self):
        self.assertRaises(TypeError, string_utils.count_vowels, 123)


class TestIsPalindrome(unittest.TestCase):

    def test_simple_palindrome(self):
        self.assertTrue(string_utils.is_palindrome("racecar"))

    def test_not_palindrome(self):
        self.assertFalse(string_utils.is_palindrome("hello"))

    def test_ignores_spaces(self):
        # spaces should be ignored
        self.assertTrue(string_utils.is_palindrome("race car"))

    def test_case_insensitive(self):
        # 'Racecar' and 'racecar' should both be palindromes
        self.assertTrue(string_utils.is_palindrome("Racecar"))

    def test_single_character(self):
        self.assertTrue(string_utils.is_palindrome("a"))

    def test_empty_string(self):
        self.assertTrue(string_utils.is_palindrome(""))


class TestCapitalizeWords(unittest.TestCase):

    def test_basic_sentence(self):
        self.assertEqual(string_utils.capitalize_words("hello world"), "Hello World")

    def test_already_capitalized(self):
        self.assertEqual(string_utils.capitalize_words("Hello World"), "Hello World")

    def test_single_word(self):
        self.assertEqual(string_utils.capitalize_words("python"), "Python")

    def test_empty_string(self):
        self.assertEqual(string_utils.capitalize_words(""), "")

    def test_raises_on_non_string(self):
        self.assertRaises(TypeError, string_utils.capitalize_words, ["hello"])

Step 4: Running Your Test File#

For your project, you will store your tests in a separate file (e.g., test_string_utils.py) and run them in your notebook.

Let’s create a test file: test_string_utils.py (provided in the same location as this notebook on PL)

# run the test file directly from the notebook using the ! shell operator
!python -m unittest test_string_utils.py -v
test_already_capitalized (test_string_utils.TestCapitalizeWords.test_already_capitalized) ... ok
test_basic_sentence (test_string_utils.TestCapitalizeWords.test_basic_sentence) ... ok
test_empty_string (test_string_utils.TestCapitalizeWords.test_empty_string) ... ok
test_raises_on_non_string (test_string_utils.TestCapitalizeWords.test_raises_on_non_string) ... ok
test_single_word (test_string_utils.TestCapitalizeWords.test_single_word) ... ok
test_basic_word (test_string_utils.TestCountVowels.test_basic_word) ... ok
test_case_insensitive (test_string_utils.TestCountVowels.test_case_insensitive) ... ok
test_empty_string (test_string_utils.TestCountVowels.test_empty_string) ... ok
test_no_vowels (test_string_utils.TestCountVowels.test_no_vowels) ... ok
test_raises_on_non_string (test_string_utils.TestCountVowels.test_raises_on_non_string) ... ok
test_case_insensitive (test_string_utils.TestIsPalindrome.test_case_insensitive) ... ok
test_empty_string (test_string_utils.TestIsPalindrome.test_empty_string) ... ok
test_ignores_spaces (test_string_utils.TestIsPalindrome.test_ignores_spaces) ... ok
test_not_palindrome (test_string_utils.TestIsPalindrome.test_not_palindrome) ... ok
test_simple_palindrome (test_string_utils.TestIsPalindrome.test_simple_palindrome) ... ok
test_single_character (test_string_utils.TestIsPalindrome.test_single_character) ... ok
test_basic_word (test_string_utils.TestReverseString.test_basic_word) ... ok
test_empty_string (test_string_utils.TestReverseString.test_empty_string) ... ok
test_palindrome_unchanged (test_string_utils.TestReverseString.test_palindrome_unchanged) ... ok
test_raises_on_non_string (test_string_utils.TestReverseString.test_raises_on_non_string) ... ok
test_single_character (test_string_utils.TestReverseString.test_single_character) ... ok

----------------------------------------------------------------------
Ran 21 tests in 0.000s

OK

What’s happening here?

  • !python -m unittest test_string_utils.py -v — the ! runs a shell command; this invokes Python’s test runner on your file

  • The -v flag means verbose — same as verbosity=2

Activity: External Module#

Recreate test_string_utils.py and run your test suite from a Jupyter notebook.

Complete this Google Form: https://forms.gle/cCNG1qHpGgEZpVQEA

What Happens When a Test Fails?#

Let’s intentionally write a broken test to see what the failure output looks like:

class TestIntentionalFailure(unittest.TestCase):

    def test_this_will_fail(self):
        # we claim reverse_string("hello") == "hello" — which is wrong!
        self.assertEqual(string_utils.reverse_string("hello"), "hello")

unittest.main(argv=[''], verbosity=2, exit=False)
test_add_negative_and_positive (__main__.TestAdd.test_add_negative_and_positive) ... ok
test_add_two_negatives (__main__.TestAdd.test_add_two_negatives) ... ok
test_add_two_positives (__main__.TestAdd.test_add_two_positives) ... ok
test_add_zeros (__main__.TestAdd.test_add_zeros) ... ok
test_almost_equal (__main__.TestAssertionMethods.test_almost_equal) ... ok
test_equal (__main__.TestAssertionMethods.test_equal) ... ok
test_false (__main__.TestAssertionMethods.test_false) ... ok
test_in (__main__.TestAssertionMethods.test_in) ... ok
test_not_equal (__main__.TestAssertionMethods.test_not_equal) ... ok
test_raises (__main__.TestAssertionMethods.test_raises) ... ERROR
test_true (__main__.TestAssertionMethods.test_true) ... ok
test_empty_list_raises_error (__main__.TestGetLastElement.test_empty_list_raises_error) ... ok
test_output_empty_list (__main__.TestGetLastElement.test_output_empty_list) ... ok
test_output_integers (__main__.TestGetLastElement.test_output_integers) ... ok
test_output_strings (__main__.TestGetLastElement.test_output_strings) ... ok
test_this_will_fail (__main__.TestIntentionalFailure.test_this_will_fail) ... FAIL
test_add_item_increases_count (__main__.TestShoppingCart.test_add_item_increases_count) ... ok
test_add_multiple_items (__main__.TestShoppingCart.test_add_multiple_items) ... ok
test_cart_contains_added_item (__main__.TestShoppingCart.test_cart_contains_added_item) ... ok
test_new_cart_is_empty (__main__.TestShoppingCart.test_new_cart_is_empty) ... ok
test_remove_item_decreases_count (__main__.TestShoppingCart.test_remove_item_decreases_count) ... ok

======================================================================
ERROR: test_raises (__main__.TestAssertionMethods.test_raises)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/4273677520.py", line 25, in test_raises
    self.assertRaises(ValueError, divide, 10, 0)
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 766, in assertRaises
    return context.handle('assertRaises', args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/envs/cogs18/lib/python3.11/unittest/case.py", line 237, in handle
    callable_obj(*args, **kwargs)
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/2956303247.py", line 2, in divide
    return num1/num2
           ~~~~^~~~~
ZeroDivisionError: division by zero

======================================================================
FAIL: test_this_will_fail (__main__.TestIntentionalFailure.test_this_will_fail)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/folders/jp/hdbfltdj035719571j9wynwm0000gn/T/ipykernel_12389/767755369.py", line 5, in test_this_will_fail
    self.assertEqual(string_utils.reverse_string("hello"), "hello")
AssertionError: 'olleh' != 'hello'
- olleh
+ hello


----------------------------------------------------------------------
Ran 21 tests in 0.016s

FAILED (failures=1, errors=1)
<unittest.main.TestProgram at 0x105ac8890>

The failure output tells you:

  1. Which test failed: test_this_will_fail

  2. What the values were: 'olleh' != 'hello'

  3. Where in the code: the line number of the failing assertion

This is why using self.assertEqual (instead of raw assert) is so helpful — the error message is much more informative.


Summary#

Concept

Key Takeaway

Unit test

Code that checks one function does what it’s supposed to

assert

The simplest way to test; raises AssertionError if False

Good test

Tests one thing, has a clear name, covers edge cases, is independent

unittest.TestCase

Organized class-based testing with helpful assertion methods

setUp()

Runs before each test; use it to avoid repeating setup code

External module

Import it like any other module; test the same way

!python -m unittest

Write and run a separate test file from inside Jupyter

For your final project, you need:

  • A module (.py file) with your functions

  • At testing suite that meaningfully tests the code of at least 3 functions/methods using unittest

  • Tests that actually run (i.e., no import errors!)