Unit Testing Fundamentals
Chapter 1: Unit Testing Fundamentals
What is a Unit Test?
A unit test verifies a single "unit" of code works correctly in isolationβtypically a function, method, or class. Unit tests shouldn't depend on databases, file systems, networks, or external systems.
Unit vs Integration vs E2E
- Unit Tests: Test individual functions in isolation. Fast, numerous, focused.
- Integration Tests: Test multiple components together. Slower, fewer.
- End-to-End Tests: Test complete workflows. Slowest, fewest.
FIRST Principles
Great unit tests follow FIRST:
- Fast: Run in milliseconds
- Isolated: No dependencies on other tests or external systems
- Repeatable: Same results every time
- Self-Validating: Clear pass/fail
- Timely: Written before or alongside code (TDD)
Writing Your First Test
Let's write a simple function and test it using pytest's conventions.
The Function to Test
The Test Function
This follows the Arrange-Act-Assert (AAA) pattern, which structures tests clearly:
- Arrange: Set up test data and preconditions
- Act: Call the function/method being tested
- Assert: Verify the result matches expectations
Try it yourself in this interactive terminal:
Test Naming Conventions
Test functions must start with test_ for pytest to discover them automatically. Use descriptive names that explain what you're testing:
β Good Names:
test_add_returns_sum_of_two_numbers()test_divide_raises_error_when_divisor_is_zero()test_user_creation_sets_default_role()
β Bad Names:
test1()- Doesn't explain what's being testedtest_func()- Too vaguetestAddition()- Doesn't follow naming convention
pytest Assertions
Pytest makes assertions incredibly powerful through assertion rewriting. When an assertion fails, pytest shows you exactly what went wrong with detailed information.
Basic Assertions
Plain assert statements work beautifully with pytest:
Advanced Assertions
Pytest provides helpers for complex scenarios:
Assertion Introspection Magic
When assertions fail, pytest's introspection shows exactly what went wrong:
def test_complex_assertion():
data = {'users': [{'name': 'Alice', 'age': 30}]}
assert data['users'][0]['age'] == 25 # This will fail
# Pytest output shows:
# AssertionError: assert 30 == 25
# + where 30 = {'name': 'Alice', 'age': 30}['age']
# + where {'name': 'Alice', 'age': 30} = [{'name': 'Alice', 'age': 30}][0]
# + where [{'name': 'Alice', 'age': 30}] = {'users': [{'name': 'Alice', 'age': 30}]}['users']This detailed breakdown makes debugging failed tests much easier.
Test Organization
Test Functions vs Test Classes
You have two main ways to organize tests:
Test Functions (simple, straightforward):
def test_login_success():
# Test successful login
pass
def test_login_invalid_password():
# Test login with wrong password
passTest Classes (group related tests):
class TestLogin:
def test_success(self):
# Test successful login
pass
def test_invalid_password(self):
# Test login with wrong password
passWhen to Use Classes
Use test classes when:
- Related tests share setup/teardown logic
- Grouping improves organization (e.g., all User tests)
- State sharing between tests is needed (use sparingly)
Use functions when:
- Tests are independent
- Setup is minimal
- Simplicity is preferred
File Organization
Organize tests to mirror your source code structure:
project/
βββ src/
β βββ myapp/
β βββ auth.py
β βββ database.py
β βββ api.py
βββ tests/
βββ test_auth.py # Tests for auth.py
βββ test_database.py # Tests for database.py
βββ test_api.py # Tests for api.pyThis makes it easy to find tests for any module.
Test Classes
Test classes provide a way to group related tests and share setup logic.
Basic Test Class
class TestCalculator:
"""Tests for calculator operations."""
def test_addition(self):
result = 2 + 2
assert result == 4
def test_subtraction(self):
result = 10 - 3
assert result == 7
def test_multiplication(self):
result = 5 * 4
assert result == 20Setup and Teardown
Use setup_method and teardown_method for test preparation and cleanup:
Class-Level Setup
For expensive operations, use setup_class (runs once per class):
class TestDatabaseOperations:
@classmethod
def setup_class(cls):
"""Connect to test database once."""
cls.db = Database.connect("test.db")
@classmethod
def teardown_class(cls):
"""Close database connection once."""
cls.db.close()
def test_insert(self):
# Use self.db
pass
def test_query(self):
# Use self.db
passTesting Edge Cases
Edge cases are boundary inputs or unusual scenarios that might break your code.
Common Edge Cases
- Empty inputs:
sum([]),process("") - None values: Test how functions handle
None - Boundary values: Minimum, maximum, just outside range
- Large numbers: Very large or very small values
- Special characters: Strings with quotes, HTML tags, etc.
Boundary Value Analysis
For ranges, test:
- Minimum valid value
- Just below minimum (invalid)
- Maximum valid value
- Just above maximum (invalid)
- Typical middle value
Example:
def test_age_boundaries():
assert is_valid_age(0) == True # Min
assert is_valid_age(-1) == False # Below
assert is_valid_age(120) == True # MaxTest Naming Conventions
Good test names are self-documenting. Use pattern: test_<what>_<condition>_<expected_result>
# β
Good
def test_withdraw_with_sufficient_funds_reduces_balance():
pass
def test_withdraw_with_insufficient_funds_raises_error():
pass
# β Poor
def test_withdraw(): # Too vague
pass
def test1(): # No context
passBest Practices:
- Explain why, not what
- Avoid abbreviations
- Long clear names are fine
Course Recommendations
Ready to master unit testing in Python? Check out these courses on paiml.com:
Test-Driven Development with Python
- Complete TDD workflow and methodology
- Red-Green-Refactor cycle mastery
- Unit testing patterns and anti-patterns
- Refactoring with confidence
- Enroll at paiml.com
Advanced pytest Techniques
- Fixtures and dependency injection
- Parametrized tests for comprehensive coverage
- Custom plugins and hooks
- Testing async code and concurrency
- Enroll at paiml.com
Clean Code with Python
- Writing testable code
- SOLID principles in Python
- Refactoring techniques
- Code smells and how to fix them
- Enroll at paiml.com
Quiz
π Test Your Knowledge: Unit Testing Fundamentals
Take this quiz to reinforce what you've learned in this chapter.