- What is unit testing — testing individual pieces of code
- Why write tests — catch bugs early, refactor with confidence
- unittest module — Python's built-in testing framework
- Assert methods — checking expected vs actual results
- Test fixtures — setup and teardown for tests
- Real-world examples — testing a calculator and a user system
What is Unit Testing?
Unit testing is the practice of testing individual pieces of your code (called "units") to make sure they work correctly. A unit is usually the smallest testable part of your code, like a function or a method.
Think of unit testing like a quality control check in a factory. Before you ship a product, you test each component to make sure it works. If a component fails, you know exactly where the problem is.
Python has a built-in module called unittest that makes writing tests easy. It's inspired by JUnit (Java's testing framework) and follows the same patterns.
💡 Key concept: Unit testing helps you catch bugs early and makes your code more reliable. It also makes it easier to change your code because you can quickly test that everything still works.
Why Write Tests?
The Benefits of Testing
Writing tests takes time, but it saves you much more time later. Here's why you should write tests.
# Why Write Tests?
print("=" * 50)
print("WHY WRITE TESTS?")
print("=" * 50)
# ============================================================
# WITHOUT TESTS - Manually Testing
# ============================================================
print("\n1. WITHOUT TESTS")
def calculate_discount(price, discount_percent):
"""Calculate discount price"""
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100")
return price * (1 - discount_percent / 100)
# Manual testing - we have to run this every time
print(" Testing calculate_discount manually:")
# Test 1: Normal discount
result = calculate_discount(100, 10)
print(f" calculate_discount(100, 10) = {result}")
if result == 90:
print(" PASS")
else:
print(" FAIL")
# Test 2: Zero discount
result = calculate_discount(100, 0)
print(f" calculate_discount(100, 0) = {result}")
if result == 100:
print(" PASS")
else:
print(" FAIL")
# Test 3: Full discount
result = calculate_discount(100, 100)
print(f" calculate_discount(100, 100) = {result}")
if result == 0:
print(" PASS")
else:
print(" FAIL")
print("\n Problems:")
print(" • We have to run tests manually")
print(" • Easy to forget a test case")
print(" • No automatic reporting")
print(" • Hard to test many cases")
# ============================================================
# WITH TESTS - Automated Testing
# ============================================================
print("\n2. WITH TESTS (unittest)")
print("""
Benefits of automated testing:
• Tests run automatically
• All tests run every time
• Clear pass/fail reporting
• Tests can be run as part of CI/CD
• Easier to find bugs
• Refactor with confidence
""")
print(" The same tests in unittest would be:")
print(" import unittest")
print("")
print(" class TestDiscount(unittest.TestCase):")
print(" def test_normal_discount(self):")
print(" self.assertEqual(calculate_discount(100, 10), 90)")
print("")
print(" def test_zero_discount(self):")
print(" self.assertEqual(calculate_discount(100, 0), 100)")
print("")
print(" def test_full_discount(self):")
print(" self.assertEqual(calculate_discount(100, 100), 0)")
# ============================================================
# KEY BENEFITS
# ============================================================
print("\n" + "-" * 30)
print("KEY BENEFITS OF UNIT TESTING")
print("-" * 30)
print("""
- Catch bugs early in development
- Refactor code with confidence
- Document how code should work
- Automate repetitive testing
- Find regression bugs
- Improve code design
- Save time in the long run
""")
Benefits of unit testing:
- Catches bugs early — find problems before they reach production
- Refactor with confidence — know that changes don't break anything
- Documents your code — tests show how code should work
- Automates testing — no more manual checking
- Improves design — testable code is better code
Quick Check: What is the main benefit of writing unit tests? (Answer: They catch bugs early and let you refactor code with confidence)
Your First Test
Writing Your First Unit Test
Let's write our first unit test using the unittest module.
# Your First Unit Test
import unittest
print("=" * 50)
print("YOUR FIRST UNIT TEST")
print("=" * 50)
# ============================================================
# CODE TO TEST
# ============================================================
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# ============================================================
# TEST CLASS
# ============================================================
class TestCalculator(unittest.TestCase):
"""Test cases for calculator functions"""
# Test add function
def test_add_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-2, -3), -5)
def test_add_zero(self):
self.assertEqual(add(5, 0), 5)
# Test multiply function
def test_multiply_positive_numbers(self):
self.assertEqual(multiply(2, 3), 6)
def test_multiply_by_zero(self):
self.assertEqual(multiply(5, 0), 0)
def test_multiply_negative_numbers(self):
self.assertEqual(multiply(-2, 3), -6)
# Test divide function
def test_divide_positive_numbers(self):
self.assertEqual(divide(6, 3), 2)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
def test_divide_negative_numbers(self):
self.assertEqual(divide(-6, 3), -2)
# ============================================================
# RUNNING TESTS
# ============================================================
print("\n1. RUNNING TESTS")
print(" To run these tests, save to a file and run:")
print(" python -m unittest filename.py")
print("")
print(" Or add this at the bottom of the file:")
print(" if __name__ == '__main__':")
print(" unittest.main()")
# ============================================================
# TEST OUTPUT EXAMPLE
# ============================================================
print("\n2. TEST OUTPUT EXAMPLE")
print("""
Test results would look like:
..........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
If a test fails:
........F.
======================================================================
FAIL: test_divide_positive_numbers (__main__.TestCalculator)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_calculator.py", line 24, in test_divide_positive_numbers
self.assertEqual(divide(6, 3), 3)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 9 tests in 0.001s
FAILED (failures=1)
""")
# ============================================================
# RUN TESTS
# ============================================================
print("\n3. RUNNING TESTS IN THIS EXAMPLE")
# To actually run the tests, uncomment:
# if __name__ == '__main__':
# unittest.main()
Your first test key points:
- Test class — inherits from
unittest.TestCase - Test methods — must start with
test_ - assert methods — check expected vs actual
- Run tests —
python -m unittest filename.py
Quick Check: What method do you use to check if two values are equal in unittest? (Answer: self.assertEqual())
Assert Methods
Different Ways to Check Results
The unittest module provides many assert methods for different types of checks.
# Assert Methods
import unittest
print("=" * 50)
print("ASSERT METHODS")
print("=" * 50)
# ============================================================
# ASSERT METHODS DEMONSTRATION
# ============================================================
class TestAsserts(unittest.TestCase):
"""Demonstrate different assert methods"""
def test_equality(self):
"""Test for equality"""
self.assertEqual(5, 5)
self.assertEqual(3.14, 3.14)
self.assertEqual("hello", "hello")
self.assertEqual([1, 2, 3], [1, 2, 3])
def test_inequality(self):
"""Test for inequality"""
self.assertNotEqual(5, 3)
self.assertNotEqual("hello", "world")
def test_truthiness(self):
"""Test for truth values"""
self.assertTrue(True)
self.assertTrue(5 > 3)
self.assertTrue("hello")
self.assertFalse(False)
self.assertFalse(5 < 3)
self.assertFalse("")
def test_is(self):
"""Test for identity"""
a = [1, 2, 3]
b = a
c = [1, 2, 3]
self.assertIs(a, b) # Same object
self.assertIsNot(a, c) # Different objects
def test_in(self):
"""Test for membership"""
self.assertIn(3, [1, 2, 3])
self.assertNotIn(5, [1, 2, 3])
self.assertIn("e", "hello")
def test_type(self):
"""Test for type"""
self.assertIsInstance(5, int)
self.assertIsInstance("hello", str)
self.assertIsInstance([1, 2], list)
def test_raises(self):
"""Test for exceptions"""
with self.assertRaises(ValueError):
int("abc")
with self.assertRaises(ZeroDivisionError):
1 / 0
def test_almost_equal(self):
"""Test for approximate equality (floats)"""
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
self.assertNotAlmostEqual(0.1 + 0.2, 0.3, places=15)
def test_greater_less(self):
"""Test for comparison"""
self.assertGreater(5, 3)
self.assertGreaterEqual(5, 5)
self.assertLess(3, 5)
self.assertLessEqual(3, 3)
# ============================================================
# COMMON ASSERT METHODS
# ============================================================
print("\n" + "-" * 30)
print("COMMON ASSERT METHODS")
print("-" * 30)
print("""
┌─────────────────────────┬────────────────────────────────────────────┐
│ Method │ What it 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 │
│ assertIsNot(a, b) │ a is not b │
│ assertIsNone(x) │ x is None │
│ assertIsNotNone(x) │ x is not None │
│ assertIn(a, b) │ a in b │
│ assertNotIn(a, b) │ a not in b │
│ assertIsInstance(a, b) │ isinstance(a, b) │
│ assertRaises(exc, func) │ func() raises exc │
│ assertAlmostEqual(a, b) │ round(a-b, 7) == 0 │
│ assertGreater(a, b) │ a > b │
│ assertLess(a, b) │ a < b │
└─────────────────────────┴────────────────────────────────────────────┘
""")
# ============================================================
# RUN DEMONSTRATION
# ============================================================
print("\nRun all tests:")
print(" python -m unittest -v asserts_demo.py")
# To run the tests:
# if __name__ == '__main__':
# unittest.main()
Assert methods key points:
- assertEqual() — checks equality
- assertTrue()/assertFalse() — checks truth values
- assertRaises() — checks for exceptions
- assertAlmostEqual() — checks floating point equality
- assertIn() — checks membership
Quick Check: Which assert method checks for exceptions? (Answer: assertRaises())
Test Fixtures (setUp/tearDown)
Setting Up and Cleaning Up for Tests
Fixtures are methods that run before and after each test. setUp() runs before each test, and tearDown() runs after each test.
# Test Fixtures - setUp and tearDown
import unittest
import os
import tempfile
print("=" * 50)
print("TEST FIXTURES")
print("=" * 50)
# ============================================================
# CODE TO TEST
# ============================================================
class FileManager:
"""Simple file manager for testing"""
def __init__(self, filename):
self.filename = filename
def write(self, content):
with open(self.filename, 'w') as f:
f.write(content)
def read(self):
with open(self.filename, 'r') as f:
return f.read()
def delete(self):
if os.path.exists(self.filename):
os.remove(self.filename)
# ============================================================
# TEST CLASS WITH FIXTURES
# ============================================================
class TestFileManager(unittest.TestCase):
"""Test FileManager with setup and teardown"""
def setUp(self):
"""Run before each test - create a test file"""
print(" setUp: Creating test file...")
self.test_file = "test_temp.txt"
self.manager = FileManager(self.test_file)
def tearDown(self):
"""Run after each test - clean up"""
print(" tearDown: Cleaning up...")
if os.path.exists(self.test_file):
os.remove(self.test_file)
def test_write_and_read(self):
"""Test writing and reading"""
print(" Running test: write_and_read")
self.manager.write("Hello, World!")
content = self.manager.read()
self.assertEqual(content, "Hello, World!")
def test_overwrite(self):
"""Test overwriting content"""
print(" Running test: overwrite")
self.manager.write("First content")
self.manager.write("Second content")
content = self.manager.read()
self.assertEqual(content, "Second content")
def test_delete(self):
"""Test deleting file"""
print(" Running test: delete")
self.manager.write("Some content")
self.manager.delete()
self.assertFalse(os.path.exists(self.test_file))
# ============================================================
# SETUP CLASS AND TEARDOWN CLASS
# ============================================================
class TestDatabase(unittest.TestCase):
"""Test with class-level setup and teardown"""
@classmethod
def setUpClass(cls):
"""Run once before all tests in the class"""
print(" setUpClass: Setting up database connection...")
cls.db_connection = "Connected to test database"
@classmethod
def tearDownClass(cls):
"""Run once after all tests in the class"""
print(" tearDownClass: Closing database connection...")
cls.db_connection = None
def test_db_connection(self):
"""Test that database is connected"""
self.assertEqual(self.db_connection, "Connected to test database")
def test_db_query(self):
"""Test a database query"""
self.assertIsNotNone(self.db_connection)
# ============================================================
# FIXTURES OVERVIEW
# ============================================================
print("\n" + "-" * 30)
print("FIXTURE OVERVIEW")
print("-" * 30)
print("""
┌─────────────────────┬────────────────────────────────────────────┐
│ Method │ When it runs │
├─────────────────────┼────────────────────────────────────────────┤
│ setUpClass() │ Once before all tests in the class │
│ setUp() │ Before each test method │
│ test_method() │ The actual test │
│ tearDown() │ After each test method │
│ tearDownClass() │ Once after all tests in the class │
└─────────────────────┴────────────────────────────────────────────┘
Use cases:
- setUp: Create test files, initialize objects
- tearDown: Clean up test files, close connections
- setUpClass: Setup database connection, load test data
- tearDownClass: Close database connection, cleanup
""")
# ============================================================
# DEMONSTRATION
# ============================================================
print("\nFixture demonstration:")
print(" python -m unittest -v fixtures_demo.py")
# To run the tests:
# if __name__ == '__main__':
# unittest.main()
Fixtures key points:
- setUp() — runs before each test
- tearDown() — runs after each test
- setUpClass() — runs once before all tests
- tearDownClass() — runs once after all tests
- Always clean up — use tearDown to remove test artifacts
Quick Check: What method runs before each test? (Answer: setUp())
Real-World Example
Testing a User Management System
# Real-World Example: Testing User Management
import unittest
import hashlib
print("=" * 60)
print("TESTING USER MANAGEMENT SYSTEM")
print("=" * 60)
# ============================================================
# USER MANAGEMENT CODE
# ============================================================
class User:
"""User class with basic functionality"""
def __init__(self, username, password, email):
self.username = username
self._password_hash = self._hash_password(password)
self.email = email
self.is_active = True
self.failed_login_attempts = 0
def _hash_password(self, password):
"""Hash password for storage"""
return hashlib.sha256(password.encode()).hexdigest()[:16]
def check_password(self, password):
"""Check if password is correct"""
return self._hash_password(password) == self._password_hash
def login(self, password):
"""Attempt to login"""
if not self.is_active:
return "Account deactivated"
if self.check_password(password):
self.failed_login_attempts = 0
return "Login successful"
else:
self.failed_login_attempts += 1
if self.failed_login_attempts >= 3:
self.is_active = False
return "Account locked due to too many failed attempts"
return f"Invalid password. {3 - self.failed_login_attempts} attempts remaining"
def change_password(self, old_password, new_password):
"""Change user password"""
if not self.check_password(old_password):
return "Invalid current password"
if len(new_password) < 8:
return "New password must be at least 8 characters"
self._password_hash = self._hash_password(new_password)
return "Password changed successfully"
def deactivate(self):
"""Deactivate the user account"""
self.is_active = False
return "Account deactivated"
def activate(self):
"""Activate the user account"""
self.is_active = True
self.failed_login_attempts = 0
return "Account activated"
class UserManager:
"""Manage users"""
def __init__(self):
self.users = {}
def add_user(self, username, password, email):
"""Add a new user"""
if username in self.users:
raise ValueError(f"User {username} already exists")
self.users[username] = User(username, password, email)
return self.users[username]
def get_user(self, username):
"""Get a user by username"""
return self.users.get(username)
# ============================================================
# TEST CLASSES
# ============================================================
class TestUser(unittest.TestCase):
"""Test the User class"""
def setUp(self):
"""Create a test user before each test"""
self.user = User("alice", "SecurePass123", "alice@example.com")
def test_create_user(self):
"""Test user creation"""
self.assertEqual(self.user.username, "alice")
self.assertEqual(self.user.email, "alice@example.com")
self.assertTrue(self.user.is_active)
self.assertEqual(self.user.failed_login_attempts, 0)
def test_check_password(self):
"""Test password checking"""
self.assertTrue(self.user.check_password("SecurePass123"))
self.assertFalse(self.user.check_password("WrongPassword"))
def test_login_success(self):
"""Test successful login"""
result = self.user.login("SecurePass123")
self.assertEqual(result, "Login successful")
self.assertEqual(self.user.failed_login_attempts, 0)
def test_login_failure(self):
"""Test failed login"""
result = self.user.login("WrongPassword")
self.assertEqual(result, "Invalid password. 2 attempts remaining")
self.assertEqual(self.user.failed_login_attempts, 1)
def test_login_lockout(self):
"""Test account lockout after 3 failed attempts"""
self.user.login("Wrong1")
self.user.login("Wrong2")
result = self.user.login("Wrong3")
self.assertEqual(result, "Account locked due to too many failed attempts")
self.assertFalse(self.user.is_active)
def test_change_password(self):
"""Test changing password"""
result = self.user.change_password("SecurePass123", "NewPass456")
self.assertEqual(result, "Password changed successfully")
self.assertTrue(self.user.check_password("NewPass456"))
self.assertFalse(self.user.check_password("SecurePass123"))
def test_change_password_wrong_old(self):
"""Test changing password with wrong old password"""
result = self.user.change_password("WrongPassword", "NewPass456")
self.assertEqual(result, "Invalid current password")
def test_change_password_short(self):
"""Test changing password to a short password"""
result = self.user.change_password("SecurePass123", "short")
self.assertEqual(result, "New password must be at least 8 characters")
def test_deactivate(self):
"""Test deactivating user"""
result = self.user.deactivate()
self.assertEqual(result, "Account deactivated")
self.assertFalse(self.user.is_active)
def test_activate(self):
"""Test activating user"""
self.user.is_active = False
result = self.user.activate()
self.assertEqual(result, "Account activated")
self.assertTrue(self.user.is_active)
def test_login_deactivated(self):
"""Test logging in with deactivated account"""
self.user.deactivate()
result = self.user.login("SecurePass123")
self.assertEqual(result, "Account deactivated")
class TestUserManager(unittest.TestCase):
"""Test the UserManager class"""
def setUp(self):
"""Create a user manager before each test"""
self.manager = UserManager()
self.manager.add_user("alice", "SecurePass123", "alice@example.com")
def test_add_user(self):
"""Test adding a user"""
user = self.manager.add_user("bob", "Pass456", "bob@example.com")
self.assertEqual(user.username, "bob")
self.assertEqual(user.email, "bob@example.com")
def test_add_user_exists(self):
"""Test adding a user that already exists"""
with self.assertRaises(ValueError):
self.manager.add_user("alice", "pass", "email")
def test_get_user(self):
"""Test getting a user"""
user = self.manager.get_user("alice")
self.assertIsNotNone(user)
self.assertEqual(user.username, "alice")
def test_get_user_not_found(self):
"""Test getting a user that doesn't exist"""
user = self.manager.get_user("nonexistent")
self.assertIsNone(user)
# ============================================================
# DEMONSTRATION
# ============================================================
print("\nRunning user management tests:")
print(" python -m unittest -v test_user_management.py")
print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("""
- Test both success and failure cases
- Use setUp to create test data
- Test edge cases (lockout, invalid input)
- Test all methods in your class
- Keep tests independent of each other
- Write tests before or alongside your code
""")
# To run the tests:
# if __name__ == '__main__':
# unittest.main()
Real-world example key points:
- Test both success and failure — test happy paths and error cases
- Use setUp() — create test data for each test
- Test edge cases — lockout, invalid input, edge conditions
- Test all methods — every method should have tests
- Independent tests — each test should run in isolation
Quick Check: Why should tests be independent of each other? (Answer: So they can run in any order and not affect each other)
Best Practices
Writing Good Tests
# Best Practices for Unit Testing
print("=" * 60)
print("BEST PRACTICES FOR UNIT TESTING")
print("=" * 60)
# ============================================================
# 1. ONE ASSERT PER TEST (if possible)
# ============================================================
print("\n1. ONE ASSERT PER TEST")
print("""
# Good - each test has one assertion
def test_add_positive():
self.assertEqual(add(2, 3), 5)
def test_add_negative():
self.assertEqual(add(-2, -3), -5)
# Bad - multiple assertions in one test
def test_add_all():
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(-2, -3), -5)
self.assertEqual(add(0, 5), 5)
""")
# ============================================================
# 2. USE MEANINGFUL TEST NAMES
# ============================================================
print("\n2. USE MEANINGFUL TEST NAMES")
print("""
# Good - clear name
def test_login_with_correct_password_returns_success():
...
# Bad - vague name
def test_login():
...
""")
# ============================================================
# 3. TEST ONE THING AT A TIME
# ============================================================
print("\n3. TEST ONE THING AT A TIME")
print("""
# Good - focused test
def test_password_change_updates_password_hash():
...
# Bad - tests multiple things
def test_password_operations():
# Tests password hashing, changing, validation
...
""")
# ============================================================
# 4. USE SETUP FOR COMMON DATA
# ============================================================
print("\n4. USE SETUP FOR COMMON DATA")
print("""
# Good - use setUp
def setUp(self):
self.user = User("alice", "pass", "email")
def test_login():
result = self.user.login("pass")
...
# Bad - create data in each test
def test_login():
user = User("alice", "pass", "email")
result = user.login("pass")
...
""")
# ============================================================
# 5. TEST EDGE CASES
# ============================================================
print("\n5. TEST EDGE CASES")
print("""
# Good - tests edge cases
def test_divide_by_zero():
with self.assertRaises(ValueError):
divide(10, 0)
def test_login_lockout():
# Test the 3rd failed attempt
...
# Don't forget:
# • Empty values
# • Zero values
# • Negative numbers
# • Maximum values
# • Invalid inputs
""")
# ============================================================
# 6. KEEP TESTS FAST
# ============================================================
print("\n6. KEEP TESTS FAST")
print("""
# Good - fast tests
def test_add():
self.assertEqual(add(2, 3), 5) # Runs instantly
# Bad - slow tests
def test_database_query():
# Database query takes 1 second
# This should be mocked
...
""")
# ============================================================
# 7. RUN TESTS REGULARLY
# ============================================================
print("\n7. RUN TESTS REGULARLY")
print("""
Run tests:
• Before committing code
• Before pushing to GitHub
• In CI/CD pipeline
• Before deployment
• Every time you change code
""")
# ============================================================
# 8. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
- One assertion per test (when possible)
- Use meaningful test names
- Test one thing at a time
- Use setUp for common test data
- Test edge cases and error conditions
- Keep tests fast and independent
- Run tests regularly
- Write tests before or alongside code (TDD)
""")
Best practices summary:
- One assert per test — makes it easier to find failures
- Meaningful names — test names should describe what's being tested
- Test one thing — each test should verify one behavior
- Use setUp() — avoid repetition in tests
- Test edge cases — don't forget error conditions
Quick Check: What's one way to make tests more maintainable? (Answer: Use setUp() to create common test data instead of repeating it in each test)
Try It Yourself
Experiment with unit testing in the editor below.
UNIT TESTING - PRACTICE
==================================================
Running tests...
You've Got It!
You now understand unit testing in Python. You know how to write tests using unittest, use assert methods, and set up test fixtures.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is unit testing in Python?
What's the difference between unittest and pytest?
How do I run unittest tests?
unittest.main() at the bottom of your file, or using the command: python -m unittest filename.py. You can also use python -m unittest discover to find and run all tests.
What is a test fixture?
Should I test private methods?
How many tests should I write?
Where to Go From Here
Now that you understand unit testing, check out these related topics:
Pytest Framework
Learn about pytest, a more modern testing framework.
Learn More →Mocking
Learn how to mock dependencies in your tests.
Learn More →Exception Handling
Learn about exceptions and how to test them.
Learn More →