Python unittest Framework Practice Problems & Exercises
Practice: unittest Framework
← Back to lessonEasy
Task: Write three test methods inside TestAdd that verify the add function works for positive integers, zero, and negative integers.
Why it matters: Every test class must inherit from unittest.TestCase. Test methods must start with test_ — the test runner discovers them automatically.
Solution:
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive(self):
self.assertEqual(add(2, 3), 5)
def test_zero(self):
self.assertEqual(add(0, 0), 0)
def test_negative(self):
self.assertEqual(add(-1, -1), -2)
if __name__ == '__main__':
unittest.main()
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive(self):
# assert add(2, 3) equals 5
pass
def test_zero(self):
# assert add(0, 0) equals 0
pass
def test_negative(self):
# assert add(-1, -1) equals -2
pass
if __name__ == '__main__':
unittest.main()
Expected Output
...\n----------------------------------------------------------------------\nRan 3 tests in 0.001s\n\nOKHints
Hint 1: Subclass `unittest.TestCase` and name every test method starting with `test_`.
Hint 2: Use `assertEqual(a, b)` to check equality, `assertTrue(expr)` for boolean checks.
Hint 3: Run with `unittest.main()` or `python -m unittest`.
Task: Complete the two test methods. test_normal checks a valid division result. test_zero_divisor asserts that dividing by zero raises ZeroDivisionError.
Solution:
import unittest
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_normal(self):
self.assertEqual(divide(10, 2), 5.0)
def test_zero_divisor(self):
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
if __name__ == '__main__':
unittest.main()
import unittest
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_normal(self):
pass # assert divide(10, 2) equals 5.0
def test_zero_divisor(self):
pass # assert ZeroDivisionError is raised
if __name__ == '__main__':
unittest.main()
Expected Output
..\n----------------------------------------------------------------------\nRan 2 tests in 0.001s\n\nOKHints
Hint 1: Use `self.assertRaises(ExceptionType)` as a context manager with the `with` statement.
Hint 2: The code inside the `with` block should raise the expected exception.
Hint 3: You can also pass a callable: `self.assertRaises(ValueError, int, "abc")`.
Task: Implement setUp to initialise self.items = [] and tearDown to clear it. Fill in both test methods to confirm each test starts with a fresh empty list.
Why it matters: Each test must be independent. setUp/tearDown guarantee isolation — no test pollutes another's state.
Solution:
import unittest
class TestWithFixtures(unittest.TestCase):
def setUp(self):
print("Setting up")
self.items = []
def tearDown(self):
print("Tearing down")
self.items.clear()
def test_append(self):
self.items.append(1)
self.assertEqual(len(self.items), 1)
def test_empty_on_start(self):
self.assertEqual(self.items, [])
if __name__ == '__main__':
unittest.main(verbosity=0)
import unittest
class TestWithFixtures(unittest.TestCase):
def setUp(self):
print("Setting up")
# create self.items = []
def tearDown(self):
print("Tearing down")
# clear self.items
def test_append(self):
# append 1 to self.items, assert length is 1
pass
def test_empty_on_start(self):
# assert self.items is empty at start of each test
pass
if __name__ == '__main__':
unittest.main(verbosity=0)
Expected Output
Setting up\nTearing down\nSetting up\nTearing down\n..\n----------------------------------------------------------------------\nRan 2 tests in 0.001s\n\nOKHints
Hint 1: `setUp` runs before each individual test method; `tearDown` runs after each one.
Hint 2: Use `setUp` to create shared state (e.g., a list or object) so tests stay independent.
Hint 3: `tearDown` is called even if the test fails — good for cleanup.
Task: Fill in all four test methods using the most appropriate assertion for each check.
Solution:
import unittest
class TestAssertions(unittest.TestCase):
def test_membership(self):
fruits = ['apple', 'banana', 'cherry']
self.assertIn('banana', fruits)
def test_none_check(self):
result = None
self.assertIsNone(result)
def test_float_approx(self):
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=2)
def test_type_check(self):
self.assertIsInstance(42, int)
if __name__ == '__main__':
unittest.main()
import unittest
class TestAssertions(unittest.TestCase):
def test_membership(self):
fruits = ['apple', 'banana', 'cherry']
# assert 'banana' is in fruits
def test_none_check(self):
result = None
# assert result is None
def test_float_approx(self):
# assert 0.1 + 0.2 is approximately 0.3 (2 decimal places)
pass
def test_type_check(self):
# assert isinstance(42, int)
pass
if __name__ == '__main__':
unittest.main()
Expected Output
....\n----------------------------------------------------------------------\nRan 4 tests in 0.001s\n\nOKHints
Hint 1: Use `assertIn(a, b)` to check membership, like `assertIn(3, [1, 2, 3])`.
Hint 2: `assertIsNone(x)` is cleaner than `assertEqual(x, None)`.
Hint 3: `assertAlmostEqual(a, b, places=2)` handles floating-point imprecision.
Medium
Task: Write four tests for BankAccount: initial balance, valid deposit, valid withdrawal, and overdraft raising ValueError.
Solution:
import unittest
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
class TestBankAccount(unittest.TestCase):
def setUp(self):
self.account = BankAccount(balance=100)
def test_initial_balance(self):
self.assertEqual(self.account.balance, 100)
def test_deposit(self):
self.account.deposit(50)
self.assertEqual(self.account.balance, 150)
def test_withdraw(self):
self.account.withdraw(30)
self.assertEqual(self.account.balance, 70)
def test_overdraft_raises(self):
with self.assertRaises(ValueError):
self.account.withdraw(200)
if __name__ == '__main__':
unittest.main()
import unittest
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
class TestBankAccount(unittest.TestCase):
def setUp(self):
self.account = BankAccount(balance=100)
def test_initial_balance(self):
pass
def test_deposit(self):
pass
def test_withdraw(self):
pass
def test_overdraft_raises(self):
pass
if __name__ == '__main__':
unittest.main()
Expected Output
....\n----------------------------------------------------------------------\nRan 4 tests in 0.001s\n\nOKHints
Hint 1: Create the `BankAccount` instance in `setUp` so every test gets a fresh account.
Hint 2: Test the happy path (deposit, withdraw) and the error path (overdraft) separately.
Hint 3: Use `assertRaises` as a context manager to catch `ValueError` on overdraft.
Task: Implement setUpClass to connect the fake DB and tearDownClass to close it. Fill in the three test methods.
Why it matters: Class-level fixtures are crucial when setup is expensive (real DB, network connection, ML model load) — you don't want to repeat it for every test.
Solution:
import unittest
class FakeDatabase:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
print("DB connected")
def close(self):
self.connected = False
print("DB closed")
def query(self, sql):
return [1, 2, 3]
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db = FakeDatabase()
cls.db.connect()
@classmethod
def tearDownClass(cls):
cls.db.close()
def test_connected(self):
self.assertTrue(self.db.connected)
def test_query_returns_list(self):
result = self.db.query("SELECT 1")
self.assertIsInstance(result, list)
def test_query_has_results(self):
result = self.db.query("SELECT 1")
self.assertEqual(len(result), 3)
if __name__ == '__main__':
unittest.main(verbosity=0)
import unittest
class FakeDatabase:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
print("DB connected")
def close(self):
self.connected = False
print("DB closed")
def query(self, sql):
return [1, 2, 3]
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db = FakeDatabase()
# connect the database
@classmethod
def tearDownClass(cls):
# close the database
pass
def test_connected(self):
pass # assert db is connected
def test_query_returns_list(self):
pass # assert query returns a list
def test_query_has_results(self):
pass # assert query has 3 items
if __name__ == '__main__':
unittest.main(verbosity=0)
Expected Output
DB connected\n...\nDB closed\n----------------------------------------------------------------------\nRan 3 tests in 0.001s\n\nOKHints
Hint 1: Decorate with `@classmethod` and receive `cls` instead of `self`.
Hint 2: `setUpClass` runs once before all tests in the class — use for expensive setup like DB connections.
Hint 3: `tearDownClass` runs once after all tests complete.
Task: Add the correct decorators to each of the three test methods as described in the comments.
Solution:
import unittest
import sys
class TestSkipping(unittest.TestCase):
@unittest.skip("Not implemented yet")
def test_not_implemented_yet(self):
self.fail("Not done")
@unittest.skipIf(sys.version_info >= (3, 12), "Requires Python < 3.12")
def test_needs_older_python(self):
self.assertTrue(True)
@unittest.expectedFailure
def test_known_bug(self):
self.assertEqual(1, 2)
if __name__ == '__main__':
unittest.main(verbosity=0)
import unittest
import sys
class TestSkipping(unittest.TestCase):
# skip this test unconditionally
def test_not_implemented_yet(self):
self.fail("Not done")
# skip only on Python 3.12+
def test_needs_older_python(self):
self.assertTrue(True)
# mark as expected failure
def test_known_bug(self):
self.assertEqual(1, 2) # bug: always fails
if __name__ == '__main__':
unittest.main(verbosity=0)
Expected Output
xss\n----------------------------------------------------------------------\nRan 3 tests in 0.001s\n\nOK (skipped=2, expected failures=1)Hints
Hint 1: Use `@unittest.skip("reason")` to unconditionally skip a test.
Hint 2: `@unittest.skipIf(condition, reason)` skips when the condition is True.
Hint 3: `@unittest.expectedFailure` marks a test that is known to fail — it passes the suite if it does fail.
Task: Inside test_various_inputs, iterate over cases and use self.subTest to assert each is_palindrome call returns the expected value.
Solution:
import unittest
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
class TestPalindrome(unittest.TestCase):
def test_various_inputs(self):
cases = [
("racecar", True),
("hello", False),
("A man a plan a canal Panama", True),
("", True),
("No lemon no melon", True),
]
for word, expected in cases:
with self.subTest(word=word):
self.assertEqual(is_palindrome(word), expected)
if __name__ == '__main__':
unittest.main()
import unittest
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
class TestPalindrome(unittest.TestCase):
def test_various_inputs(self):
cases = [
("racecar", True),
("hello", False),
("A man a plan a canal Panama", True),
("", True),
("No lemon no melon", True),
]
for word, expected in cases:
# use subTest here
pass
if __name__ == '__main__':
unittest.main()
Expected Output
.\n----------------------------------------------------------------------\nRan 1 test in 0.001s\n\nOKHints
Hint 1: Use `with self.subTest(i=i)` (or any label) inside a loop.
Hint 2: If one sub-test fails, the loop continues — you see all failures at once rather than stopping at the first.
Hint 3: SubTests are ideal for testing a function against a table of input/output pairs.
Hard
Task: Implement build_suite to create a suite containing only test_add and test_multiply from TestMath, then run it with TextTestRunner.
Why it matters: Manual suites let CI pipelines run targeted subsets — e.g., smoke tests only, or tests for a specific module — without running the full suite.
Solution:
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
def test_multiply(self):
self.assertEqual(3 * 4, 12)
def test_subtract(self):
self.assertEqual(5 - 3, 2)
def build_suite():
suite = unittest.TestSuite()
suite.addTest(TestMath("test_add"))
suite.addTest(TestMath("test_multiply"))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=0)
runner.run(build_suite())
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
def test_multiply(self):
self.assertEqual(3 * 4, 12)
def test_subtract(self):
self.assertEqual(5 - 3, 2)
# Build a suite that runs only test_add and test_multiply
# (not test_subtract)
def build_suite():
suite = unittest.TestSuite()
# add only the two desired tests
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=0)
runner.run(build_suite())
Expected Output
Ran 2 tests in 0.001s\n\nOKHints
Hint 1: Use `unittest.TestSuite()` and `suite.addTest(TestClass("method_name"))` to add individual tests.
Hint 2: `unittest.TestLoader().loadTestsFromTestCase(Class)` loads all tests from a class at once.
Hint 3: Run the suite with `unittest.TextTestRunner(verbosity=2).run(suite)`.
Task: Implement addFailure in CollectingResult so it stores the test's short description in self.custom_failures and prints CUSTOM: <name> FAILED.
Solution:
import unittest
class CollectingResult(unittest.TestResult):
def __init__(self):
super().__init__()
self.custom_failures = []
def addFailure(self, test, err):
super().addFailure(test, err)
name = test.shortDescription() or str(test)
self.custom_failures.append(name)
print(f"CUSTOM: {test._testMethodName} FAILED")
class TestSample(unittest.TestCase):
def test_pass(self):
self.assertEqual(1, 1)
def test_fail(self):
self.assertEqual(1, 2)
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(TestSample)
result = CollectingResult()
suite.run(result)
print(f"Total failures recorded: {len(result.custom_failures)}")
import unittest
class CollectingResult(unittest.TestResult):
def __init__(self):
super().__init__()
self.custom_failures = []
def addFailure(self, test, err):
super().addFailure(test, err)
# record test name in self.custom_failures
# print: CUSTOM: <test_name> FAILED
class TestSample(unittest.TestCase):
def test_pass(self):
self.assertEqual(1, 1)
def test_fail(self):
self.assertEqual(1, 2)
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(TestSample)
result = CollectingResult()
suite.run(result)
print(f"Total failures recorded: {len(result.custom_failures)}")
Expected Output
CUSTOM: test_fail FAILED\nTotal failures recorded: 1Hints
Hint 1: Subclass `unittest.TestResult` and override `addFailure(test, err)` to hook into failure reporting.
Hint 2: `err` is a tuple of `(type, value, traceback)` — same as `sys.exc_info()`.
Hint 3: Pass your custom result to `suite.run(result)` instead of using `TextTestRunner`.
Task: Fill in the three test methods using make_temp_file() as shown. Verify file creation, correct round-trip reading, and an empty file case.
Why it matters: Real integration tests touch the filesystem. addCleanup ensures temp files are always removed — even when tests fail — keeping the test environment clean.
Solution:
import unittest
import tempfile
import os
def write_lines(filepath, lines):
with open(filepath, 'w') as f:
for line in lines:
f.write(line + '\n')
def read_lines(filepath):
with open(filepath, 'r') as f:
return [l.rstrip('\n') for l in f]
class TestFileIO(unittest.TestCase):
def make_temp_file(self):
fd, path = tempfile.mkstemp(suffix='.txt')
os.close(fd)
self.addCleanup(os.unlink, path)
return path
def test_write_creates_file(self):
path = self.make_temp_file()
write_lines(path, ['hello', 'world'])
self.assertTrue(os.path.exists(path))
def test_read_returns_correct_lines(self):
path = self.make_temp_file()
write_lines(path, ['alpha', 'beta', 'gamma'])
self.assertEqual(read_lines(path), ['alpha', 'beta', 'gamma'])
def test_empty_file(self):
path = self.make_temp_file()
write_lines(path, [])
self.assertEqual(read_lines(path), [])
if __name__ == '__main__':
unittest.main()
import unittest
import tempfile
import os
def write_lines(filepath, lines):
with open(filepath, 'w') as f:
for line in lines:
f.write(line + '
')
def read_lines(filepath):
with open(filepath, 'r') as f:
return [l.rstrip('
') for l in f]
class TestFileIO(unittest.TestCase):
def make_temp_file(self):
fd, path = tempfile.mkstemp(suffix='.txt')
os.close(fd)
self.addCleanup(os.unlink, path)
return path
def test_write_creates_file(self):
path = self.make_temp_file()
write_lines(path, ['hello', 'world'])
# assert the file exists
pass
def test_read_returns_correct_lines(self):
path = self.make_temp_file()
write_lines(path, ['alpha', 'beta', 'gamma'])
# assert read_lines returns the same list
pass
def test_empty_file(self):
path = self.make_temp_file()
write_lines(path, [])
# assert read_lines returns []
pass
if __name__ == '__main__':
unittest.main()
Expected Output
...\n----------------------------------------------------------------------\nRan 3 tests in 0.005s\n\nOKHints
Hint 1: Use `tempfile.NamedTemporaryFile(delete=False)` to create a real temp file, then register cleanup with `self.addCleanup(os.unlink, path)`.
Hint 2: `addCleanup` is called after `tearDown` even if the test raises — more reliable than `try/finally`.
Hint 3: Test both the file write path and the file read path in separate test methods to keep them focused.
