Unit Testing with Pytest
Stop hoping your code works. Prove it. Mastering TDD, Fixtures, and Mocks.
1. The Big Idea (ELI5)
👶 Explain Like I'm 10: The Crash Test Dummy
Imagine you build a car.
- Manual Testing: You drive it yourself into a wall to see if the airbag works. If it fails, you die (Production Crash).
- Automated Testing: You put a Crash Test Dummy in the car and smash it into a wall in a simulator. You do this 1,000 times before a real human ever sits inside.
- The Goal: Catch bugs in the simulator (Dev Environment), not on the highway (Production).
2. Why Pytest? (The Industry Standard)
Python has a built-in `unittest` module (based on Java's JUnit). It requires Classes and Boilerplate.Pytest is modern, functional, and requires zero boilerplate.
# 1. Standard Python Function (The Code)
def add(a, b):
return a + b
# 2. Pytest Function (The Test)
# Just start the function name with 'test_'
def test_add_simple():
result = add(2, 3)
assert result == 5 # No self.assertEqual() needed!
def test_add_negative():
assert add(-1, -1) == -23. Fixtures: The Setup & Teardown Magic
Real apps need data (Databases, User Objects). creating them in every test is repetitive.Fixtures let you define setup logic once and inject it into any test.
import pytest
# Define a fixture using the decorator
@pytest.fixture
def dummy_user():
return {"id": 1, "username": "admin", "role": "superuser"}
# Inject it by name!
def test_superuser_permissions(dummy_user):
# Pytest automatically calls dummy_user() and passes the result
assert dummy_user["role"] == "superuser"Fixtures can also handle Teardown (Cleanup) using `yield`.
@pytest.fixture
def db_connection():
print("🔌 Connecting to DB...")
db = connect_to_db()
yield db # Test runs here!
print("🔌 Closing DB connection...")
db.close()4. Deep Dive: TDD (Test Driven Development)
TDD is a philosophy: Write the Test BEFORE the Code.
- Red: Write a test for a feature that doesn't exist yet. Run it. It fails (Red).
- Green: Write the minimum amount of code to make the test pass.
- Refactor: Clean up the code while ensuring the test stays Green.
Benefit: This forces you to design the API usage before implementation, resulting in much cleaner, more usable interfaces.
5. Mocking: Faking the Outside World
What if your function calls a Credit Card API? You can't charge a real card every time you run tests! You use a Mock to fake the response.
from unittest.mock import patch
import requests
def get_exchange_rate():
# Real network call
resp = requests.get("https://api.forex.com/usd")
return resp.json()["rate"]
# The Test
# We patch 'requests.get' so it never hits the internet
@patch("requests.get")
def test_get_exchange_rate(mock_get):
# 1. Setup the fake return value
mock_get.return_value.json.return_value = {"rate": 1.5}
# 2. Run code
rate = get_exchange_rate()
# 3. Verify
assert rate == 1.5
mock_get.assert_called_once() # Ensure we actually called the API6. Parametrization: Running 1 Test 100 Times
Don't write 10 copy-pasted tests. Use `@pytest.mark.parametrize` to run the same logic with different inputs.
@pytest.mark.parametrize("input, expected", [
("hello", "HELLO"),
("world", "WORLD"),
("Python", "PYTHON"),
("", "")
])
def test_str_upper(input, expected):
assert input.upper() == expected