Unit Testing Best Practices
Unit tests are your safety net. They catch regressions early, document expected behavior, and give you the confidence to refactor. But poorly written tests can be worse than no tests at all. Here's how to write unit tests that actually help.
Follow the AAA Pattern
Every unit test should follow the Arrange-Act-Assert pattern, which makes tests readable and self-documenting.
def test_calculate_total_applies_discount():
# Arrange
cart = ShoppingCart()
cart.add_item("laptop", price=1000)
cart.add_item("mouse", price=25)
coupon = Coupon(code="SUMMER25", discount=0.25)
# Act
total = cart.calculate_total(coupon)
# Assert
expected = (1000 + 25) * 0.75 # 768.75
assert abs(total - expected) < 0.01
Test One Thing Per Test
Each test should verify a single behavior. If a test fails, you should immediately know what went wrong.
# Bad — testing multiple behaviors in one test
def test_user_login():
# Tests validation, database lookup, password hashing, AND session creation
# Good — focused tests
def test_login_rejects_invalid_password():
def test_login_creates_session_on_success():
def test_login_locks_account_after_five_failures():
Use Descriptive Test Names
A test name should read like a sentence describing the expected behavior.
// Good test names that double as documentation
test("should return empty array when list is empty");
test("should throw error when dividing by zero");
test("should update user email and persist changes");
Test Edge Cases, Not Just Happy Paths
The happy path is the easiest case. The real value of testing comes from covering edge cases:
def test_calculate_average():
# Happy path
assert calculate_average([10, 20, 30]) == 20.0
# Edge cases
assert calculate_average([]) == 0.0 # Empty list
assert calculate_average([42]) == 42.0 # Single element
assert calculate_average([-5, 5]) == 0.0 # Negative numbers
Mock External Dependencies
Unit tests should test your code in isolation. Mock databases, APIs, and file systems.
from unittest.mock import patch, MagicMock
def test_send_notifications_uses_api():
mock_api = MagicMock()
mock_api.post.return_value.status_code = 200
with patch("myapp.notifications.NotificationAPI", return_value=mock_api):
send_notifications(["user1", "user2"])
mock_api.post.assert_called_once()
assert mock_api.post.call_args[0][1] == ["user1", "user2"]
Conclusion
Great tests are fast, readable, and focused. They should be so clear that a new team member can understand what your code is supposed to do just by reading them. Invest time in writing good tests — they pay dividends every time a regression strikes.