Python Code Testing
Let’s look at how to test our Python code and follow the code coverage as much as possible.
- How to follow the MVC pattern in FastAPI
- How to write Pythonic code
- Types of testing with
pytest
- Usage of
patching
,monkeypatching
,fixture
, andmocking
🚀 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
- Model →
app/models/
- View →
app/routes/
(FastAPI endpoints) - Controller →
app/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
- Pytest:
https://docs.pytest.org/en/latest/ - pytest-cov (code coverage plugin):
https://pytest-cov.readthedocs.io/en/latest/ - pytest-mock (for using
mock.patch
cleanly):
https://pytest-mock.readthedocs.io/en/latest/ - unittest.mock (standard Python mocking):
https://docs.python.org/3/library/unittest.mock.html - FastAPI Testing:
https://fastapi.tiangolo.com/tutorial/testing/
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: strclass 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