Sitemap

Python Code Testing

4 min readMay 13, 2025

Let’s look at how to test our Python code and follow the code coverage as much as possible.

  1. How to follow the MVC pattern in FastAPI
  2. How to write Pythonic code
  3. Types of testing with pytest
  4. Usage of patching, monkeypatching, fixture, and mocking

🚀 How to Follow the MVC Pattern in FastAPI

FastAPI doesn’t enforce a strict MVC structure, but you can follow an organized MVC-like structure:

🔹 MVC Directory Structure Example

app/

├── models/ # ORM models (e.g., SQLAlchemy)
│ └── user.py

├── schemas/ # Pydantic schemas (DTOs)
│ └── user.py

├── controllers/ # Business logic (aka services)
│ └── user_controller.py

├── routes/ # Route definitions
│ └── user_routes.py

├── main.py # Entry point
└── database.py # DB engine/session

🔹 MVC Mapping

  • Modelapp/models/
  • Viewapp/routes/ (FastAPI endpoints)
  • Controllerapp/controllers/ (business logic)

🐍 How to Write Pythonic Code

Follow these tips for clean, maintainable code:

✅ Do’s

  • Use list comprehensions: squares = [x**2 for x in range(10)]
  • Use unpacking: a, b = b, a
  • Use f-strings: f"Hello, {name}!"
  • Follow PEP8: spacing, naming conventions
  • Write modular code using functions and classes

❌ Don’ts

  • Avoid deeply nested code
  • Avoid hardcoded values (use config files or env vars)
  • Avoid long functions (>40 lines ideally)

🧪 Code Coverage & Testing with pytest

🔹 Types of Tests

  • Unit Tests: Test isolated functions/methods
  • Integration Tests: Test multiple modules working together
  • Functional Tests: Test end-to-end use cases
  • Regression Tests: Ensure new code doesn’t break existing functionality

🔹 Measuring Code Coverage

pip install pytest pytest-cov
pytest --cov=app tests/

🧰 Testing Utilities in pytest

1. Fixtures

  • Provide pre-loaded data or setup
import pytest
@pytest.fixture
def user_data():
return {"username": "test", "email": "test@test.com"}

2. Mocking

  • Replace parts of the system to isolate the test
from unittest.mock import Mock
mock_service = Mock()
mock_service.get_user.return_value = {"name": "Mocked"}

3. Patching

  • Temporarily replace object/function for the test
from unittest.mock import patch
@patch("app.controllers.user_controller.get_user")
def test_get_user(mock_get_user):
mock_get_user.return_value = {"name": "Patched"}

4. Monkeypatching

  • Provided by pytest to change attributes during test
def test_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test123")

📌 Summary

  • Use a clean MVC layout in FastAPI
  • Write Pythonic code using idioms and best practices
  • Use pytest with fixtures, mocking, patching, and monkeypatching for robust test coverage
  • Measure test coverage with pytest-cov

Here’s how you can quickly start with pytest, pytest-cov, mocking, and code coverage for your FastAPI MVC app:

🚀 Quick Setup Commands

# Step 1: Install required packages
pip install pytest pytest-cov pytest-mock
# Step 2: Run all tests
pytest
# Step 3: Run tests with coverage
pytest --cov=app --cov-report=term-missing
# Optional: For HTML coverage report
pytest --cov=app --cov-report=html

🔗 Official Docs for Reference

Some code example to understand in better way

🧭 FastAPI MVC Pattern and Testing Guide with pytest, Mocking, and Integration Tests (No Real DB)

📁 Folder Structure (MVC + Testing)

project/
├── app/
│ ├── controllers/
│ │ └── user_controller.py
│ ├── models/
│ │ └── user.py
│ ├── schemas/
│ │ └── user.py
│ ├── routes/
│ │ └── user_routes.py
│ ├── database.py
│ └── main.py
├── tests/
│ ├── controllers/
│ │ └── test_user_controller.py
│ ├── routes/
│ │ └── test_user_routes.py
│ ├── conftest.py
│ └── integration/
│ └── test_app.py

1️⃣ app/schemas/user.py

from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
email: str
class UserOut(BaseModel):
id: int
name: str
email: str

2️⃣ app/models/user.py

from pydantic import BaseModel
# Simulating a DB model with static data
class User(BaseModel):
id: int
name: str
email: str
@staticmethod
def get_users():
return [
User(id=1, name="Alice", email="alice@test.com"),
User(id=2, name="Bob", email="bob@test.com")
]

3️⃣ app/controllers/user_controller.py

from app.models.user import User
def list_users():
return User.get_users()

4️⃣ app/routes/user_routes.py

from fastapi import APIRouter
from app.controllers.user_controller import list_users
from app.schemas.user import UserOut
from typing import List
router = APIRouter()@router.get("/users", response_model=List[UserOut])
def get_users():
return list_users()

5️⃣ app/main.py

from fastapi import FastAPI
from app.routes import user_routes
app = FastAPI()
app.include_router(user_routes.router)

✅ Unit Testing with pytest

📌 tests/controllers/test_user_controller.py

from app.controllers.user_controller import list_users
def test_list_users():
users = list_users()
assert len(users) == 2
assert users[0].name == "Alice"

📌 tests/routes/test_user_routes.py

from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)def test_get_users():
response = client.get("/users")
assert response.status_code == 200
assert response.json()[0]["name"] == "Alice"

🧪 Mocking and Patching (without DB)

📌 Patch the Controller Function

from unittest.mock import patch
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)@patch("app.controllers.user_controller.list_users")
def test_get_users_mocked(mock_list_users):
mock_list_users.return_value = [{"id": 1, "name": "Mock", "email": "mock@test.com"}]
response = client.get("/users")
assert response.status_code == 200
assert response.json()[0]["name"] == "Mock"

🧪 Monkeypatching with pytest

📌 tests/controllers/test_user_controller.py

def fake_get_users():
return [{"id": 99, "name": "Fake", "email": "fake@test.com"}]
def test_monkeypatch_user(monkeypatch):
from app.models import user
monkeypatch.setattr(user.User, "get_users", fake_get_users)
from app.controllers.user_controller import list_users
result = list_users()
assert result[0]["name"] == "Fake"

🔌 Fixtures in pytest (shared test data)

📌 tests/conftest.py

import pytest
@pytest.fixture
def test_user():
return {"id": 1, "name": "Test", "email": "test@test.com"}

📌 Use the Fixture

def test_with_fixture(test_user):
assert test_user["email"] == "test@test.com"

🔁 Integration Test (without Real DB)

📌 tests/integration/test_app.py

from fastapi.testclient import TestClient
from unittest.mock import patch
from app.main import app
client = TestClient(app)def fake_users():
return [{"id": 101, "name": "IntTest", "email": "int@test.com"}]
@patch("app.controllers.user_controller.list_users", side_effect=fake_users)
def test_full_integration(mock_list_users):
response = client.get("/users")
assert response.status_code == 200
assert response.json()[0]["name"] == "IntTest"

📊 Measure Code Coverage

pytest --cov=app tests/

✅ Summary Checklist

  • Follow MVC: models, schemas, controllers, routes
  • Use pytest for unit tests
  • Use mock, patch, monkeypatch to avoid real DB calls
  • Create integration tests using TestClient
  • Measure coverage with pytest-cov

--

--

Dhiraj Patra
Dhiraj Patra

Written by Dhiraj Patra

AI Strategy, Generative AI, AI & ML Consulting, Product Development, Startup Advisory, Data Architecture, Data Analytics, Executive Mentorship, Value Creation

No responses yet