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. ✨
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
functionor 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!
- Red: Write a test that fails (because the code isn’t there yet).
- Green: Write just enough code to make that test pass.
- 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_databaseor creating atemporary filefor each test.1 2
def setUp(self): self.db_conn = connect_to_test_db()
- Example: Connecting to a
tearDown(): Runs after every single test, even if the test fails. It ensures complete cleanup of resources created bysetUp().- Example: Closing
db_connor deleting thetemporary file.1 2
def tearDown(self): self.db_conn.close()
- Example: Closing
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_schemaor launching amock server.1 2 3
@classmethod def setUpClass(cls): cls.shared_resource = setup_shared_db_schema()
- Example: Setting up a
tearDownClass(): Runs only once after all tests in the class have finished. It cleans up everythingsetUpClass()created.- Example: Tearing down the
shared_database_schemaor stopping themock server.1 2 3
@classmethod def tearDownClass(cls): cls.shared_resource.teardown()
- Example: Tearing down the
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
TestCaseinheritance! - Less Boilerplate: Forget
import unittestandself.assertEqual(). Use standardassertstatements. - 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 ▶️
- Install:
pip install pytest - 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_correctlyto 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 bothunittest and pytest. You'll test login functionality, password validation, session management, and error handling to ensure secure, reliable authentication.🎯 Requirements
- Create an
AuthenticationSystemclass with methods:register_user(username, password, email)- Register new userslogin(username, password)- Authenticate and return session tokenvalidate_password(password)- Check password strength (min 8 chars, 1 uppercase, 1 digit)logout(session_token)- Invalidate session
- Write
unittesttest cases with propersetUpandtearDownmethods - Write equivalent
pytesttests using fixtures - Test edge cases: duplicate usernames, invalid passwords, expired sessions
- Achieve at least 85% code coverage
- Use the AAA pattern (Arrange, Act, Assert) for all tests
💡 Implementation Hints
- Store users in a dictionary:
{'username': {'password': 'hashed', 'email': 'user@example.com'}} - Use
hashlibto hash passwords (e.g.,hashlib.sha256(password.encode()).hexdigest()) - Generate session tokens with
uuid.uuid4().hex - For pytest, create fixtures like
@pytest.fixture def auth_system() - Use
assertRaisesorpytest.raisesto test exception handling - 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.parametrizeto test multiple password scenarios - Level 5: Create a mock database using
unittest.mockinstead of in-memory dictionary - Level 6: Add integration tests that combine multiple authentication operations
📚 Learning Goals
- Master
unittest.TestCasestructure 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.