Post

22. Testing in Python

🧪 Master Python testing with unittest and pytest! Learn test-driven development, fixtures, coverage tools, and best practices to build reliable, production-ready applications. ✨

22. Testing in Python

What we will learn in this post?

  • 👉 Introduction to Testing
  • 👉 unittest - Python's Built-in Testing Framework
  • 👉 unittest Assertions
  • 👉 setUp and tearDown Methods
  • 👉 Introduction to pytest
  • 👉 pytest Fixtures
  • 👉 Test Coverage and Best Practices

Software Testing: Your Software’s Best Friend! 💖

Ever wondered how your favorite apps just work flawlessly? That’s thanks to software testing! It’s like a thorough quality check, ensuring our digital creations are reliable, smooth, and free from unexpected glitches. We find potential issues early, making sure users have a fantastic experience every time.

Why Testing Matters: Building Trust & Quality 🛡️

Imagine a payment app crashing during a transaction or a game full of bugs. Testing prevents these nightmares! It builds immense trust with users, saves companies money by fixing problems proactively, and ultimately delivers high-quality products. Untested bugs can be very costly!

Types of Testing: Different Checks for Different Parts 🔍

Software testing isn’t just one thing; it has layers:

  • Unit Testing: Checks the smallest pieces of code, like a single function or method.
  • Integration Testing: Ensures different parts of the system work together smoothly (e.g., login interacting with database).
  • Functional Testing: Verifies the entire application behaves as expected from a user’s perspective (e.g., “Can I successfully add an item to my cart?”).

Test-Driven Development (TDD): Code with Confidence! ✨

TDD is a smart way to build software. Instead of writing code then testing it, you write tests first!

  1. Red: Write a test that fails (because the code isn’t there yet).
  2. Green: Write just enough code to make that test pass.
  3. Refactor: Improve the code’s design without changing its behavior.

This cycle leads to cleaner, more robust, and easier-to-maintain code.

graph LR
    A["🔴 Write Failing Test"]:::style1 --> B["🟢 Write Minimal Code"]:::style2
    B --> C{"✅ Run All Tests?"}:::style3
    C -- "All pass" --> D["✨ Refactor Code"]:::style4
    C -- "Tests fail" --> B
    D --> E["🔄 Next Feature"]:::style5
    E --> A

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Hello, Code Explorers! 🧪 Dive into Python’s unittest!

Imagine a built-in Python tool that helps you ensure your code works as expected. That’s unittest! It’s a powerful framework for writing automated tests, helping you catch bugs early and maintain code quality. Think of it as your personal code quality assistant!

Creating Test Cases with unittest.TestCase ✍️

To test your code, you’ll define a test case. This is a Python class that subclasses unittest.TestCase. Each test case groups related tests together, making it easy to organize and run comprehensive test suites for your applications.

  • Code Example:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    import unittest
    
    # The simple function we want to test
    def add(a, b):
        return a + b
    
    class TestMyMath(unittest.TestCase):
        # Your test methods will go here
        pass
    

Writing Test Methods: test_ Power! 💪

Inside your test case, define methods that perform specific checks. Each test method must start with test_. You use assertion methods (provided by unittest.TestCase) to verify if results are correct.

  • Common Assertion: self.assertEqual(expected, actual) checks if two values are equal.

  • Test Method Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    # ... (previous 'add' function and TestMyMath class) ...
    
    class TestMyMath(unittest.TestCase):
        def test_add_two_positives(self):
            """Test adding two positive numbers."""
            self.assertEqual(add(2, 3), 5) # Is 2 + 3 equal to 5?
            self.assertEqual(add(10, 0), 10)
    
        def test_add_with_negatives(self):
            """Test adding with negative numbers."""
            self.assertEqual(add(-1, 1), 0)
    

Time to See Results: Running Your Tests! ▶️

To run your tests, add a simple block to your test file. When executed, unittest.main() discovers and runs all methods starting with test_ within unittest.TestCase subclasses.

  • How to Run:
    1
    2
    3
    4
    
    # ... (all your test code) ...
    
    if __name__ == '__main__':
        unittest.main()
    

    Save this code as my_tests.py. Then, open your terminal and run: python my_tests.py

This will print a report, showing dots (.) for passes or F for failures!


Testing Your Python Code with Unittest Assertions! ✨

Hey there, fellow coder! 👋 When you’re building awesome Python programs, you want to be sure they work exactly as intended. That’s where unittest assertions come in handy! They’re like little detectives, checking if your code’s actual behavior matches your expected behavior. If an assertion fails, you know there’s a bug to squash!

Your Essential Assertion Toolkit 🛠️

Here are some common assertions you’ll use:

assertEqual (Checks for Equality) ✅

This assertion makes sure two values are precisely the same. Example: self.assertEqual(my_function(2, 3), 5) checks if my_function returns 5.

assertNotEqual (Checks for Inequality) 🚫

Opposite of assertEqual, this ensures two values are different. Example: self.assertNotEqual(status_code, 404) confirms the status_code isn’t 404.

assertTrue (Checks if True) 👍

Use this when you expect a condition or value to be True. Example: self.assertTrue(is_logged_in) verifies is_logged_in is True.

assertFalse (Checks if False) 👎

This confirms a condition or value is False. Example: self.assertFalse(has_permission) ensures has_permission is False.

assertIn (Checks for Presence) 🔍

Verifies if an item exists within a collection (like a list or string). Example: self.assertIn('milk', shopping_list) checks if 'milk' is in shopping_list.

assertRaises (Checks for Expected Errors) 💥

This special one lets you confirm that your code raises a specific error when it should. Example: with self.assertRaises(ValueError): process_data('') expects a ValueError for empty data.


How Assertions Fit in Your Test Flow 🌊

graph TD
    A["🚀 Start Test"]:::style1 --> B["⚙️ Call Function"]:::style2
    B --> C{"🔍 Outcome Expected?"}:::style3
    C -- "Yes" --> D["✅ Test Passed"]:::style4
    C -- "No" --> E["❌ Test Failed"]:::style5
    E --> F["📋 Show Error Report"]:::style1

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Real-World Example: API Response Validator 🌐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import unittest
import requests

class TestAPIResponses(unittest.TestCase):
    """Test API endpoints for a weather service."""
    
    def test_weather_endpoint_returns_200(self):
        """Verify weather API returns successful status code."""
        response = requests.get('https://api.weather.example.com/current')
        self.assertEqual(response.status_code, 200)
    
    def test_response_contains_temperature(self):
        """Ensure temperature data is present in response."""
        response = requests.get('https://api.weather.example.com/current')
        data = response.json()
        self.assertIn('temperature', data)
        self.assertTrue(isinstance(data['temperature'], (int, float)))
    
    def test_invalid_city_raises_error(self):
        """Confirm API raises error for invalid city."""
        with self.assertRaises(requests.exceptions.HTTPError):
            response = requests.get('https://api.weather.example.com/current?city=InvalidCity123')
            response.raise_for_status()

setUp() & tearDown(): Your Test Prep & Cleanup Crew! 🛠️

Ever wanted your tests to be super independent and always start from a clean slate? That’s where setUp() and tearDown() come in! They’re essential for managing test initialization and cleanup, ensuring your tests are reliable.

🚀 Individual Test Setup/Cleanup

These methods run for each individual test:

  • setUp(): Runs before every single test method in your class. Perfect for creating fresh, isolated resources your test needs.
    • Example: Connecting to a test_database or creating a temporary file for each test.
      1
      2
      
      def setUp(self):
      self.db_conn = connect_to_test_db()
      
  • tearDown(): Runs after every single test, even if the test fails. It ensures complete cleanup of resources created by setUp().
    • Example: Closing db_conn or deleting the temporary file.
      1
      2
      
      def tearDown(self):
      self.db_conn.close()
      
graph TD
    A["🏁 Test Class Start"]:::style1 --> B["🔧 setUp() runs"]:::style2
    B --> C["🧪 test_method() runs"]:::style3
    C --> D["🧹 tearDown() runs"]:::style4
    D --> E{"🔄 Another Test?"}:::style5
    E -- "Yes" --> B
    E -- "No" --> F["✅ Test Class End"]:::style1

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

🏫 Class-Level Setup/Cleanup

For resources needed once for all tests within a class, use these:

  • setUpClass(): Runs only once before any test in the class starts. Ideal for expensive, shared setups.
    • Example: Setting up a shared_database_schema or launching a mock server.
      1
      2
      3
      
      @classmethod
      def setUpClass(cls):
      cls.shared_resource = setup_shared_db_schema()
      
  • tearDownClass(): Runs only once after all tests in the class have finished. It cleans up everything setUpClass() created.
    • Example: Tearing down the shared_database_schema or stopping the mock server.
      1
      2
      3
      
      @classmethod
      def tearDownClass(cls):
      cls.shared_resource.teardown()
      
graph TD
    A["🏁 Test Suite Start"]:::style1 --> B["🎬 setUpClass() ONCE"]:::style2
    B --> C["🔧 setUp() Test 1"]:::style3
    C --> D["🧪 Test 1 runs"]:::style4
    D --> E["🧹 tearDown() Test 1"]:::style5
    E --> F["🔧 setUp() Test 2"]:::style3
    F --> G["🧪 Test 2 runs"]:::style4
    G --> H["🧹 tearDown() Test 2"]:::style5
    H --> I["..."]:::style1
    I --> J["🎬 tearDownClass() ONCE"]:::style2
    J --> K["✅ Test Suite End"]:::style1

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Real-World Example: Database Test Suite 🗄️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import unittest
import sqlite3

class TestDatabaseOperations(unittest.TestCase):
    """Test suite for database CRUD operations."""
    
    @classmethod
    def setUpClass(cls):
        """Set up test database schema once for all tests."""
        cls.conn = sqlite3.connect(':memory:')
        cls.cursor = cls.conn.cursor()
        cls.cursor.execute('''
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                username TEXT UNIQUE NOT NULL,
                email TEXT NOT NULL
            )
        ''')
        cls.conn.commit()
    
    def setUp(self):
        """Clean database before each test."""
        self.cursor.execute('DELETE FROM users')
        self.conn.commit()
    
    def test_insert_user(self):
        """Test inserting a new user."""
        self.cursor.execute(
            'INSERT INTO users (username, email) VALUES (?, ?)',
            ('john_doe', 'john@example.com')
        )
        self.conn.commit()
        
        self.cursor.execute('SELECT * FROM users WHERE username = ?', ('john_doe',))
        user = self.cursor.fetchone()
        self.assertIsNotNone(user)
        self.assertEqual(user[1], 'john_doe')
    
    def test_duplicate_username_fails(self):
        """Verify duplicate username raises integrity error."""
        self.cursor.execute(
            'INSERT INTO users (username, email) VALUES (?, ?)',
            ('jane_doe', 'jane@example.com')
        )
        self.conn.commit()
        
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                'INSERT INTO users (username, email) VALUES (?, ?)',
                ('jane_doe', 'different@example.com')
            )
            self.conn.commit()
    
    @classmethod
    def tearDownClass(cls):
        """Close database connection after all tests."""
        cls.conn.close()

pytest: Modern Testing Made Easy! ✨

Meet pytest, the go-to Python testing framework for its simplicity and powerful features. It’s designed to make writing tests fun and efficient, offering a significant upgrade from unittest.

Why pytest Over unittest? 🚀

  • Simpler Syntax: Write tests as plain functions, not classes. No more TestCase inheritance!
  • Less Boilerplate: Forget import unittest and self.assertEqual(). Use standard assert statements.
  • Powerful Fixtures: Reusable setup/teardown code, easily injected.
  • Rich Ecosystem: Thousands of plugins extend its capabilities.
  • Better Output: Detailed failure reports with context and variable values for faster debugging.

Simple Test Function Syntax 📝

Writing tests is straightforward. Just define functions starting with test_ and use Python’s built-in assert statement.

1
2
3
4
5
6
# test_example.py
def test_addition():
    assert 1 + 1 == 2

def test_string_uppercase():
    assert "hello".upper() == "HELLO"

Basic Usage & Running Tests ▶️

  1. Install: pip install pytest
  2. Run: Open your terminal in the test directory and type: pytest

pytest automatically discovers test files (starting with test_ or ending with _test.py) and runs all test functions!

graph LR
    A["🔍 pytest Discovery"]:::style1 --> B["📁 Find test_*.py"]:::style2
    B --> C["🧪 Collect test_*() functions"]:::style3
    C --> D["▶️ Run Tests"]:::style4
    D --> E["📊 Generate Report"]:::style5

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

pytest Fixtures: Reusable Test Setup 🔧

Fixtures are pytest’s powerful way to set up test preconditions. They’re functions decorated with @pytest.fixture that provide data or resources to your tests.

Basic Fixture Usage 📦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

@pytest.fixture
def sample_user():
    """Fixture providing a sample user dictionary."""
    return {
        'username': 'test_user',
        'email': 'test@example.com',
        'age': 25
    }

def test_user_has_email(sample_user):
    """Test that fixture provides user with email."""
    assert 'email' in sample_user
    assert sample_user['email'] == 'test@example.com'

def test_user_age_is_valid(sample_user):
    """Test that user age is a positive number."""
    assert sample_user['age'] > 0

Fixture Scopes: Control Lifetime ⏰

Fixtures can have different scopes to control when they’re created and destroyed:

  • function (default): Created/destroyed for each test
  • class: Shared across all tests in a class
  • module: Shared across all tests in a file
  • session: Created once for entire test session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pytest
import requests

@pytest.fixture(scope="session")
def api_client():
    """Session-scoped fixture for API client."""
    session = requests.Session()
    session.headers.update({'Authorization': 'Bearer test-token'})
    yield session
    session.close()

@pytest.fixture(scope="function")
def clean_test_data():
    """Function-scoped fixture for clean test data."""
    data = {'items': []}
    yield data
    # Cleanup after test
    data['items'].clear()
graph TD
    A["🎯 Test Session Start"]:::style1 --> B["🔧 session fixture created"]:::style2
    B --> C["📦 module fixture created"]:::style3
    C --> D["🧪 Test 1 starts"]:::style4
    D --> E["⚙️ function fixture created"]:::style5
    E --> F["✅ Test 1 ends"]:::style4
    F --> G["🗑️ function fixture destroyed"]:::style5
    G --> H["🧪 Test 2 starts"]:::style4
    H --> I["⚙️ new function fixture"]:::style5
    I --> J["✅ Test 2 ends"]:::style4
    J --> K["🗑️ module fixture destroyed"]:::style3
    K --> L["🗑️ session fixture destroyed"]:::style2
    L --> M["🏁 Session End"]:::style1

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Real-World Example: E-Commerce Test Fixtures 🛒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import pytest
from decimal import Decimal

@pytest.fixture
def empty_shopping_cart():
    """Fixture providing an empty shopping cart."""
    return {'items': [], 'total': Decimal('0.00')}

@pytest.fixture
def product_catalog():
    """Fixture providing sample product catalog."""
    return [
        {'id': 1, 'name': 'Laptop', 'price': Decimal('999.99')},
        {'id': 2, 'name': 'Mouse', 'price': Decimal('29.99')},
        {'id': 3, 'name': 'Keyboard', 'price': Decimal('79.99')}
    ]

@pytest.fixture
def cart_with_items(empty_shopping_cart, product_catalog):
    """Fixture providing cart with pre-added items."""
    cart = empty_shopping_cart
    cart['items'].append(product_catalog[0])  # Add laptop
    cart['items'].append(product_catalog[1])  # Add mouse
    cart['total'] = sum(item['price'] for item in cart['items'])
    return cart

def test_empty_cart_total(empty_shopping_cart):
    """Test that empty cart has zero total."""
    assert empty_shopping_cart['total'] == Decimal('0.00')
    assert len(empty_shopping_cart['items']) == 0

def test_cart_calculates_total(cart_with_items):
    """Test cart correctly calculates item totals."""
    expected_total = Decimal('999.99') + Decimal('29.99')
    assert cart_with_items['total'] == expected_total

def test_add_item_to_cart(empty_shopping_cart, product_catalog):
    """Test adding item increases cart total."""
    cart = empty_shopping_cart
    keyboard = product_catalog[2]
    
    cart['items'].append(keyboard)
    cart['total'] += keyboard['price']
    
    assert len(cart['items']) == 1
    assert cart['total'] == Decimal('79.99')

Unmasking Your Code’s Untested Corners with Coverage Tools! 🕵️‍♀️

Ever wonder how much of your code is actually tested? That’s where Code Coverage steps in! It’s like a detective for your tests, showing you which lines of code your tests touch, and more importantly, which they don’t.

Meet the Coverage Crew 🛠️

We use tools like coverage.py and pytest-cov (a plugin for pytest) to measure this. They help ensure your tests are thorough, giving you confidence in your software.

How to Measure Coverage 📊

Run your tests with pytest-cov to see a summary and generate detailed reports:

1
pytest --cov=your_module --cov-report=html

This command runs your pytest tests, tracks coverage for your_module, and creates an interactive htmlcov/index.html report!

What Coverage Percentage to Aim For? 🎯

While 100% seems ideal, a realistic and healthy goal is 80-90%. Over 90% can sometimes mean testing trivial code or spending too much effort for diminishing returns. Focus on critical paths and complex logic.

Smart Testing Best Practices ✨

Writing good tests is crucial for reliable coverage.

The AAA Pattern (Arrange, Act, Assert) 🚦

This pattern makes tests clear and organized:

graph LR
    A["🔧 Arrange<br/>Setup Data"]:::style1 --> B["⚡ Act<br/>Call Function"]:::style2
    B --> C["✅ Assert<br/>Verify Result"]:::style3

    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Let’s see it in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# my_app.py
def add(a, b):
    return a + b # This line is covered by the test
# def subtract(a, b): # This function won't be covered by our example test
#     return a - b

# test_my_app.py
from my_app import add

def test_add_positive_numbers_correctly(): # Descriptive test name
    # Arrange
    num1, num2 = 5, 3
    # Act
    result = add(num1, num2)
    # Assert
    assert result == 8 # An independent check
  • Test Naming: Use descriptive names like test_add_positive_numbers_correctly to easily understand what each test checks.
  • Independence: Each test should run independently, without relying on the order or state set by other tests.

🎯 Hands-On Assignment: Build a User Authentication Test Suite 🚀

📝 Your Mission

Create a comprehensive test suite for a user authentication system using both unittest and pytest. You'll test login functionality, password validation, session management, and error handling to ensure secure, reliable authentication.

🎯 Requirements

  1. Create an AuthenticationSystem class with methods:
    • register_user(username, password, email) - Register new users
    • login(username, password) - Authenticate and return session token
    • validate_password(password) - Check password strength (min 8 chars, 1 uppercase, 1 digit)
    • logout(session_token) - Invalidate session
  2. Write unittest test cases with proper setUp and tearDown methods
  3. Write equivalent pytest tests using fixtures
  4. Test edge cases: duplicate usernames, invalid passwords, expired sessions
  5. Achieve at least 85% code coverage
  6. Use the AAA pattern (Arrange, Act, Assert) for all tests

💡 Implementation Hints

  1. Store users in a dictionary: {'username': {'password': 'hashed', 'email': 'user@example.com'}}
  2. Use hashlib to hash passwords (e.g., hashlib.sha256(password.encode()).hexdigest())
  3. Generate session tokens with uuid.uuid4().hex
  4. For pytest, create fixtures like @pytest.fixture def auth_system()
  5. Use assertRaises or pytest.raises to test exception handling
  6. Run coverage with: pytest --cov=authentication --cov-report=html

🚀 Example Input/Output

# Example usage and expected behavior
auth = AuthenticationSystem()

# Registration
auth.register_user('john_doe', 'SecurePass123', 'john@example.com')
# Expected: User registered successfully

# Login
token = auth.login('john_doe', 'SecurePass123')
# Expected: Returns session token like 'a3f5b2c8d1e4f7a9b2c5d8e1f4a7b0c3'

# Invalid password
auth.validate_password('weak')
# Expected: Raises ValueError('Password must be at least 8 characters')

# Duplicate registration
auth.register_user('john_doe', 'AnotherPass456', 'different@example.com')
# Expected: Raises ValueError('Username already exists')

# Logout
auth.logout(token)
# Expected: Session invalidated, subsequent use raises error

🏆 Bonus Challenges

  • Level 2: Add email validation using regex patterns
  • Level 3: Implement session expiration (tokens valid for 30 minutes)
  • Level 4: Add parameterized tests using @pytest.mark.parametrize to test multiple password scenarios
  • Level 5: Create a mock database using unittest.mock instead of in-memory dictionary
  • Level 6: Add integration tests that combine multiple authentication operations

📚 Learning Goals

  • Master unittest.TestCase structure with setUp/tearDown lifecycle 🎯
  • Understand pytest fixtures and their scope (function, class, module, session) ✨
  • Apply AAA pattern for clean, readable test structure 🔄
  • Practice testing edge cases and error conditions 🔗
  • Measure and improve code coverage with pytest-cov 🛠️
  • Compare unittest vs pytest approaches for the same functionality 📊

💡 Pro Tip: This authentication testing pattern is used in production systems like Django (django.contrib.auth.tests), Flask-Login test suites, and FastAPI security testing examples!

Share Your Solution! 💬

Completed the project? Post your code in the comments below! Show us your Python testing mastery! 🚀✨


Conclusion: Testing for Reliable Python Applications 🎓

Testing is the cornerstone of professional Python development, transforming code from functional to production-ready. By mastering unittest and pytest, applying TDD principles, using fixtures effectively, and maintaining high code coverage, you’ll build robust applications that users trust and that stand the test of time in real-world deployments.

This post is licensed under CC BY 4.0 by the author.