Python Code Testing

 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


Comments

Popular posts from this blog

Self-contained Raspberry Pi surveillance System Without Continue Internet

COBOT with GenAI and Federated Learning

AI in Education: Embracing Change for Future-Ready Learning