Unit Testing#
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’; Ferrariadding 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>isTrue→ nothing happens (the test “passes silently”)If
<expression>isFalse→ Python raises anAssertionError
# 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 |
|---|---|
|
|
|
|
|
|
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[], zero0Negative 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.TestCaseclass name starts with
Test…and then includes what it’s testingEach 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
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 passedFAIL= test ran but the assertion was wrongERROR= 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
Write at least 3 unit test methods using
unittest.TestCasethat would catch the bugRun your tests — do any fail?
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[], zero0Negative 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 |
|---|---|
|
Returns the string reversed |
|
Counts vowels (a, e, i, o, u) |
|
Checks if a string reads the same forwards and backwards |
|
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 fileThe
-vflag means verbose — same asverbosity=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:
Which test failed:
test_this_will_failWhat the values were:
'olleh' != 'hello'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 |
|
The simplest way to test; raises |
Good test |
Tests one thing, has a clear name, covers edge cases, is independent |
|
Organized class-based testing with helpful assertion methods |
|
Runs before each test; use it to avoid repeating setup code |
External module |
Import it like any other module; test the same way |
|
Write and run a separate test file from inside Jupyter |
For your final project, you need:
A module (
.pyfile) with your functionsAt testing suite that meaningfully tests the code of at least 3 functions/methods using
unittestTests that actually run (i.e., no import errors!)