Unittest, assertEqual, setUp & tearDown Explained - Python Unit Testing Tutorial #28
Video: Unittest, assertEqual, setUp & tearDown Explained - Python Unit Testing Tutorial #28 by Taught by Celeste AI - AI Coding Coach
Python Unit Testing with unittest
class TestX(unittest.TestCase):groups tests. Methods starting withtest_are auto-discovered.assertEqual,assertTrue,assertRaisesmake claims.setUp/tearDownfor per-test fixtures.python -m unittestruns the suite.
Tests catch bugs before users do. Python's unittest module is built in — no install needed.
A first test
# math_tools.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_math_tools.py
import unittest
from math_tools import add, divide
class TestMathTools(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(-1, 1), 0)
def test_divide(self):
self.assertEqual(divide(10, 2), 5.0)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()
Run it:
python -m unittest test_math_tools.py
Output:
...
Ran 3 tests in 0.001s
OK
Each . is a passing test. F for failure, E for error.
Test discovery
python -m unittest # discover all test_*.py in the cwd
python -m unittest discover # same
python -m unittest discover tests/ # specific directory
python -m unittest test_math_tools # specific module
python -m unittest test_math_tools.TestMathTools.test_add # specific test
By default, unittest finds files matching test*.py and within them, classes inheriting from TestCase and methods starting with test_.
Assertions
| Assertion | Checks |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) |
bool(x) is True |
assertFalse(x) |
bool(x) is False |
assertIs(a, b) |
a is b (identity) |
assertIsNone(x) |
x is None |
assertIn(a, b) |
a in b |
assertIsInstance(a, cls) |
isinstance(a, cls) |
assertAlmostEqual(a, b) |
floats nearly equal |
assertRaises(Error) |
block raises Error |
assertGreater(a, b) |
a > b |
assertCountEqual(a, b) |
same elements (any order) |
These give better failure messages than assert a == b. When a test fails, you see what each was, not just "False is not True."
setUp and tearDown
class TestGradebook(unittest.TestCase):
def setUp(self):
self.gb = Gradebook()
self.gb.add_student("Alice")
self.gb.add_student("Bob")
def tearDown(self):
pass # cleanup if needed
def test_add_student(self):
self.gb.add_student("Charlie")
self.assertIn("Charlie", self.gb.students)
def test_add_grade(self):
self.gb.add_grade("Alice", 95)
self.assertEqual(self.gb.students["Alice"], [95])
setUp runs before each test method. tearDown runs after. Use them for per-test fixtures: fresh objects, temporary files, mocks.
For class-wide setup that should run once:
@classmethod
def setUpClass(cls):
cls.expensive_resource = create_expensive_thing()
@classmethod
def tearDownClass(cls):
cls.expensive_resource.cleanup()
assertRaises
def test_invalid_grade(self):
with self.assertRaises(ValueError):
self.gb.add_grade("Alice", 150)
Verify that a block raises a specific exception. The test passes only if the exception type matches.
To inspect the exception:
with self.assertRaises(ValueError) as ctx:
divide(10, 0)
self.assertIn("zero", str(ctx.exception))
Skipping and expected failures
import sys
@unittest.skip("not yet implemented")
def test_unfinished(self):
...
@unittest.skipIf(sys.platform == "win32", "Linux only")
def test_unix_only(self):
...
@unittest.expectedFailure
def test_known_bug(self):
self.assertEqual(1, 2)
Useful for in-progress code, platform-specific tests, and tracking known-broken cases without breaking the suite.
Mocking with unittest.mock
from unittest.mock import patch, MagicMock
@patch("module.requests.get")
def test_fetch(self, mock_get):
mock_get.return_value.json.return_value = {"data": [1, 2, 3]}
result = fetch_data()
self.assertEqual(result, [1, 2, 3])
mock_get.assert_called_once()
@patch replaces a name with a MagicMock for the duration of the test. Use it to fake external calls (HTTP, DB, time) so tests are fast and deterministic.
MagicMock() returns another MagicMock for any attribute or method call. You configure return values and verify calls.
Verbosity and output
python -m unittest -v # verbose: shows each test name
python -m unittest -f # fail fast: stop after first failure
python -m unittest -k pattern # only tests matching pattern
Verbose output:
test_add (test_math_tools.TestMathTools) ... ok
test_divide (test_math_tools.TestMathTools) ... ok
test_divide_by_zero (test_math_tools.TestMathTools) ... ok
Ran 3 tests in 0.001s
OK
A test for a stateful class
class TestGradebook(unittest.TestCase):
def setUp(self):
self.gb = Gradebook()
self.gb.add_student("Alice")
def test_add_grade(self):
self.gb.add_grade("Alice", 95)
self.assertEqual(self.gb.students["Alice"], [95])
def test_get_average(self):
self.gb.add_grade("Alice", 90)
self.gb.add_grade("Alice", 80)
self.assertEqual(self.gb.get_average("Alice"), 85.0)
def test_invalid_grade(self):
with self.assertRaises(ValueError):
self.gb.add_grade("Alice", 150)
Each test gets a fresh Gradebook from setUp. They run in any order, never depend on each other.
Test layout conventions
myproject/
src/
myproject/
gradebook.py
tests/
__init__.py # may be empty
test_gradebook.py
Common patterns:
- One
test_X.pyper module under test. - Test class names
TestXfor clarity. - Test method names describe the case:
test_add_grade_invalid_value.
Pytest is the popular alternative — works with the same test_*.py discovery, cleaner syntax (assert x == y instead of self.assertEqual), better fixtures. For new projects, pytest is the standard choice. But unittest works everywhere without install.
Coverage
pip install coverage
coverage run -m unittest discover
coverage report
coverage html # opens htmlcov/index.html — line-by-line
Coverage measures which lines of your code are exercised by tests. 100% coverage is a goal, not a guarantee — covered code can still have bugs. But low coverage almost certainly hides them.
What good tests look like
- Fast. Milliseconds. No network, no DB unless integration test.
- Independent. Each test stands alone. Order doesn't matter.
- Repeatable. Same input, same result, every time.
- Clear. Test name says what's being verified.
- One thing per test. Easier to diagnose failures.
Common stumbles
Test method without test_ prefix. Discovery skips it.
setUp not running. Did you spell it setup or setUP? Capital S, capital U: setUp.
Mutable class attribute as fixture. class TestX: items = [] shares across tests. Always create fresh in setUp.
Tests that depend on each other. Test A leaves state, Test B depends on it. Order matters → flaky tests. Use setUp to isolate.
Asserting on test side effects. A test that prints "Looks good!" but never asserts isn't a test. Always make a claim.
Mocking too much. Tests pass but don't reflect real behavior. Mock external systems; test your real code.
Skipping if __name__ == "__main__":. Without it, python -m unittest still finds tests, but python test_file.py doesn't run anything.
What's next
Lesson 29: type hints. Static typing in Python — List[int], Optional, Union, mypy.
Recap
unittest.TestCase subclass with test_* methods. Assertions with assertEqual, assertRaises, etc. for clear failure messages. setUp/tearDown for per-test fixtures, setUpClass for once-per-class. python -m unittest discovers and runs. unittest.mock for replacing external dependencies. Pytest is the popular alternative for new projects. Tests should be fast, independent, repeatable, and one-thing-per-test.
Next lesson: type hints.