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
pytestto 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-cov (code coverage plugin):
https://pytest-cov.readthedocs.io/en/latest/ -
pytest-mock (for using
mock.patchcleanly):
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: 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
pytestfor unit tests -
Use
mock,patch,monkeypatchto avoid real DB calls -
Create integration tests using
TestClient -
Measure coverage with
pytest-cov
Comments