Python Mastery: Complete Beginner to Professional
HomeInsightsCoursesPythonUnit Testing with Pytest

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.

PYTHON
# 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) == -2

3. 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.

PYTHON
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`.

PYTHON
@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.

  1. Red: Write a test for a feature that doesn't exist yet. Run it. It fails (Red).
  2. Green: Write the minimum amount of code to make the test pass.
  3. 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.

PYTHON
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 API

6. 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.

PYTHON
@pytest.mark.parametrize("input, expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("Python", "PYTHON"),
    ("", "")
])
def test_str_upper(input, expected):
    assert input.upper() == expected

What's Next?

Automated tests catch bugs before they happen. But what do you do when a bug does happen in production? Next, we learn Professional Debugging with PDB.