diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..14b749b --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,42 @@ +name: Python CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test and Lint + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿงพ Checkout repository + uses: actions/checkout@v4 + + - name: ๐Ÿ Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: ๐Ÿ“ฆ Install Poetry + run: | + pip install poetry + poetry config virtualenvs.create false + + - name: ๐Ÿ“„ Install dependencies + run: poetry install --no-interaction --no-root + + - name: โœ… Run tests + run: poetry run pytest -v + + - name: ๐Ÿงผ Check formatting + run: | + pip install black + black --check . + + - name: ๐Ÿ” Lint with ruff + run: | + pip install ruff + ruff check . diff --git a/README.md b/README.md index ef5ec57..a8194f6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Built to showcase best practices in Python backend development and serve as a so - ๐Ÿ”€ Routing with versioned endpoints - ๐Ÿ“„ OpenAPI docs auto-generated (`/docs`, `/redoc`) - โœ… Data validation and typing with Pydantic v2 -- ๐Ÿงช Async-ready test setup with `pytest`, `httpx`, `pytest-asyncio` (in progress) +- ๐Ÿงช Async-ready test setup with `pytest`, `httpx`, `pytest-asyncio` - ๐Ÿณ Minimal Docker support - ๐Ÿงฐ Designed as a reusable starter template diff --git a/app/api/users.py b/app/api/users.py index 47053e6..a0e7bde 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter + from app.models.user import User, UserCreate from app.services.user_service import save_user, get_all_users diff --git a/app/main.py b/app/main.py index bc014b6..39431a8 100644 --- a/app/main.py +++ b/app/main.py @@ -2,9 +2,10 @@ import sys from fastapi import FastAPI + from app.api import users -log_format = '%(asctime)s | %(levelname)-8s | %(name)-12s - %(message)s' +log_format = "%(asctime)s | %(levelname)-8s | %(name)-12s - %(message)s" logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=log_format) app = FastAPI(title="Python FastAPI Template", version="0.1.0") diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api/test_users.py b/tests/api/test_users.py new file mode 100644 index 0000000..f2f95e1 --- /dev/null +++ b/tests/api/test_users.py @@ -0,0 +1,27 @@ +import httpx +import pytest + +from app.main import app + + +@pytest.mark.asyncio +async def test_create_user(): + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.post( + "/api/v1/users/", json={"name": "Simone", "email": "simone@example.com"} + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Simone" + assert data["email"] == "simone@example.com" + + +@pytest.mark.asyncio +async def test_get_users(): + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.get("/api/v1/users/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/services/test_user_service.py b/tests/services/test_user_service.py new file mode 100644 index 0000000..36c4afc --- /dev/null +++ b/tests/services/test_user_service.py @@ -0,0 +1,19 @@ +import pytest + +from app.models.user import UserCreate +from app.services.user_service import save_user, get_all_users + + +@pytest.mark.asyncio +async def test_save_user(): + user = UserCreate(name="Simone", email="simone@example.com") + saved_user = await save_user(user) + assert saved_user.name == "Simone" + assert saved_user.email == "simone@example.com" + + +@pytest.mark.asyncio +async def test_get_all_users_empty(): + users = await get_all_users() + assert isinstance(users, list) + assert len(users) > 0