Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • 📝 Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • 📝 Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • 📝 Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • 📝 Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • 📝 While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • 📝 For Each Assignments
    • 📝 All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • 📝 Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • 📝 Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • 📝 Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • 📝 Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • 📝 Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • 📝 Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • 📝 Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • 📝 Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • 📝 Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • 📝 Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • 📝 Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • 📝 Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • 📝 Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • 📝 Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • 📝 Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • 📝 Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • 📝 Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • 📝 Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • 📝 Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Unit Testing

Python Unit Testing - Complete Guide

Write reliable code with automated testing.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What is Unit Testing?
  • Why Write Tests?
  • Your First Test
  • Assert Methods
  • Test Fixtures (setUp/tearDown)
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • 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?

1

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

2

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

3

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)

4

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

5

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

6

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.

Loading Pyodide... 0%
Python Code Editor
==================================================
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:

1. What module is used for unit testing in Python?
2. What method do you use to check if two values are equal?
3. What method runs before each test?
4. What assert method checks for exceptions?
5. Why should tests be independent?

Frequently Asked Questions

What is unit testing in Python? ▼

Unit testing is the practice of testing individual units of code (like functions and methods) to verify they work correctly. Python's unittest module provides a framework for writing and running these tests.

What's the difference between unittest and pytest? ▼

unittest is Python's built-in testing framework. pytest is a third-party framework that's more concise and has more features. Many developers prefer pytest for its simpler syntax and powerful plugins.

How do I run unittest tests? ▼

You can run tests by adding 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? ▼

A test fixture is the setup and cleanup code that runs before and after tests. setUp() runs before each test, tearDown() runs after each test. setUpClass() and tearDownClass() run once for the entire test class.

Should I test private methods? ▼

Generally, you should test public methods only. Private methods are implementation details that should be tested through the public interface. If a private method is complex, consider making it public or moving it to a separate class.

How many tests should I write? ▼

Write enough tests to cover all functionality. Aim for good test coverage (80-100%). Test both happy paths (normal behavior) and edge cases (errors, boundary conditions). The goal is to catch bugs early and make refactoring safe.

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 →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Standard Library Assignments Next: Pytest →