From 57fa7dfc120ae3392fde50fda6aa245effe81414 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 8 Feb 2026 21:29:48 +0100 Subject: [PATCH 1/2] adding test to project --- .gitignore | 1 + backend/app/main.py | 32 ++++++++++++-------- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes backend/db/base.py | 2 +- backend/pytest.ini | 15 ++++++++++ backend/tests/__init__.py | 0 backend/tests/conftest.py | 54 ++++++++++++++++++++++++++++++++++ backend/tests/test_api.py | 19 ++++++++++++ backend/tests/test_crud.py | 0 backend/tests/test_schemas.py | 0 backend/tests/test_tasks.py | 0 11 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 backend/pytest.ini create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_api.py create mode 100644 backend/tests/test_crud.py create mode 100644 backend/tests/test_schemas.py create mode 100644 backend/tests/test_tasks.py diff --git a/.gitignore b/.gitignore index 93f7557..1cbf5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ coverage.xml # Unit test / pytest # ========================= .pytest_cache/ +test_api.db # ========================= # Environment files diff --git a/backend/app/main.py b/backend/app/main.py index 642da21..c4ab772 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,17 +6,25 @@ from typing import List from dotenv import load_dotenv from fastapi.middleware.cors import CORSMiddleware -from datetime import datetime, timezone - -# Automatic db creation +from contextlib import asynccontextmanager from db.base import Base, engine -Base.metadata.create_all(bind=engine) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Funtion gets executed during 'uvicorn main:app command' + + lifespan needs a task, yield and optional task after yield + """ + Base.metadata.create_all(bind=engine) + yield # Async functions need async sessions load_dotenv() -app = FastAPI() +app = FastAPI(lifespan=lifespan) origins = [ "http://localhost:5173" @@ -33,13 +41,13 @@ @app.get('/') -async def index(): +def index(): return {"message": "App is working"} @app.get('/reminders', response_model=List[ReminderRead], description="Returns all reminders in the app") # Get all reminders -async def all_reminders(db: Session = Depends(get_db)): +def all_reminders(db: Session = Depends(get_db)): reminders = crud.get_all_reminders(db) if not reminders: return [] @@ -48,7 +56,7 @@ async def all_reminders(db: Session = Depends(get_db)): @app.post('/create', response_model=ReminderRead, description="Create reminder") # Create reminder -async def create_reminder( +def create_reminder( reminder: ReminderCreate, db: Session = Depends(get_db) ): @@ -66,7 +74,7 @@ async def create_reminder( @app.get('/reminders/{id}', response_model=ReminderRead, description="Get reminders by its ID") # Get specific reminder -async def get_reminder(id: int, db: Session = Depends(get_db)): +def get_reminder(id: int, db: Session = Depends(get_db)): reminder = crud.get_reminder(db, id) if not reminder: raise HTTPException(status_code=404, detail="Object was not found") @@ -75,7 +83,7 @@ async def get_reminder(id: int, db: Session = Depends(get_db)): @app.put('/reminders/{id}', response_model=ReminderRead, description="Edit reminder") # Update reminder values -async def update_reminder( +def update_reminder( id: int, reminder: ReminderUpdate, db: Session = Depends(get_db) @@ -90,7 +98,7 @@ async def update_reminder( @app.delete('/reminders/{id}', description="Delete reminder by its ID") # Delete reminder -async def delete_reminder(id: int, db: Session = Depends(get_db)): +def delete_reminder(id: int, db: Session = Depends(get_db)): deleted = crud.delete_reminder(db, id) if not deleted: raise HTTPException(status_code=404, detail="Object was not found") @@ -100,7 +108,7 @@ async def delete_reminder(id: int, db: Session = Depends(get_db)): @app.delete('/reminders', description="Delete all reminders") # Delete all reminders -async def delete_all_reminders(db: Session = Depends(get_db)): +def delete_all_reminders(db: Session = Depends(get_db)): deleted_reminders = crud.delete_all(db) if not deleted_reminders: return {'message': 'No reminders to delete'} diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 6f42a9210ff9e2145a36dac53da3e77fc9926778..1fb8c1f4fd95de505c2ab632e5c8dc3703621d74 100644 GIT binary patch delta 26 hcmZo@U~Fh$+|X&vs%61&*>-ZDaWtd)=2ymIJOFtC2(JJD delta 26 hcmZo@U~Fh$+|X&vs%F7(Y3JlV<7h_Z&998bcmRHE2_gUh diff --git a/backend/db/base.py b/backend/db/base.py index b098aa4..2c2b635 100644 --- a/backend/db/base.py +++ b/backend/db/base.py @@ -8,4 +8,4 @@ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) # If not False there would be an error while doing multiple threads SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() +Base = declarative_base() \ No newline at end of file diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..ff920f2 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,15 @@ +# Config file for pytest + +[tool:pytest] # Header for pytest +pythonpath = backend +testpaths = backend/tests # Shows tests folder +python_files = test_*.py # Files only that starts with test_ +python_functions = test_* # Only functions that start with test_ +# Additional options +addopts = + -v # verbose - more details while running tests + --strict-markers # Python will alert about using unregistered marker + --tb=short # traceback for error, short version + +# Section for registering markers +markers = diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..b705982 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,54 @@ +import pytest +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy import create_engine +from db.base import Base +from fastapi.testclient import TestClient +from app.main import app +from db.deps import get_db + +# Database for testing +TEST_DATABASE_URL = "sqlite:///./test_api.db" + + +@pytest.fixture # Pytest prepare for other functions +def test_db() -> Session: + ''' + Creates clean database for each test. + After completing database is removed. + ''' + + engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False}) # Create engine in RAM + + from db.models import Reminder # noqa: F401, import for Base + + + Base.metadata.create_all(bind=engine) # Creating db + + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # Connections factory + db = TestingSessionLocal() + + try: + yield db + finally: + db.close() + Base.metadata.drop_all(bind=engine) # Clear after test + + +@pytest.fixture +def client(test_db: Session) -> TestClient: + """ + TestClient FastAPI with test database. + Used to test endpoints, simulates user's action. + """ + def override_get_db(): + try: + yield test_db + finally: + pass + + app.dependency_overrides[get_db] = override_get_db # Switching database to test unit + + with TestClient(app) as c: + yield c # Creating client for an action + + app.dependency_overrides.clear() # Switching back to database diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..1446003 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from app.main import get_db + + +def test_create_reminder(client: TestClient): + payload = { + "title": 'Laundry', + "description": "Remember about laundry", + "due_to": "24-12-2026 12:00", + "email": "test@example", + "alert_type": "minutes" + } + + response = client.post("/create", json=payload) + + assert response.status_code == 200 + data = response.json() + assert data['title'] == "Laundry" + assert "id" in data diff --git a/backend/tests/test_crud.py b/backend/tests/test_crud.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_schemas.py b/backend/tests/test_schemas.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_tasks.py b/backend/tests/test_tasks.py new file mode 100644 index 0000000..e69de29 From 4ad8989ad2909ab7a3778c9beb662ed2a19b06b6 Mon Sep 17 00:00:00 2001 From: kuba Date: Mon, 9 Feb 2026 12:12:46 +0100 Subject: [PATCH 2/2] adding tests and github actions --- .github/workflows/tests.yml | 31 +++++++++ backend/app/main.py | 4 +- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes backend/db/test.db | Bin 12288 -> 12288 bytes backend/pytest.ini | 4 +- backend/tests/conftest.py | 16 ++++- backend/tests/test_api.py | 127 +++++++++++++++++++++++++++++++--- backend/tests/test_crud.py | 0 backend/tests/test_schemas.py | 0 backend/tests/test_tasks.py | 0 10 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 backend/tests/test_crud.py delete mode 100644 backend/tests/test_schemas.py delete mode 100644 backend/tests/test_tasks.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..08972ae --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +# Config file for GitHub Actions + +name: Backend Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest # Fastest option + + steps: + - name: Checkout code + uses: actions/checkout@v6 # Copies code on VM + + - name: Set up Python + uses: actions/setup-python@v5 # Official GitHub tool + with: + python-version: '3.12' + + - name: Install dependencies + run: | # Everything below will be treated as one line + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + pip install pytest-cov httpx + + - name: Run tests + env: + PYTHONPATH: ${{ github.workspace }}/backend # github.workspace is a root dir, safe config + run: | + cd backend + pytest --cov=app --cov-report=term-missing \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index c4ab772..0eef418 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,7 +13,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): """ - Funtion gets executed during 'uvicorn main:app command' + Funtion gets executed during 'uvicorn main:app command - server start' lifespan needs a task, yield and optional task after yield """ @@ -60,7 +60,7 @@ def create_reminder( reminder: ReminderCreate, db: Session = Depends(get_db) ): - + new_reminder = crud.create_reminder( db, title=reminder.title, diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 1fb8c1f4fd95de505c2ab632e5c8dc3703621d74..6cb3eb81edead6a0781bf37eac66ecf36794b0a2 100644 GIT binary patch delta 29 kcmZo@U~Fh$+|Xvs!NkeKZp7HkI=Rm{no(!-D`PPp0DIjC8vp Session: ''' engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False}) # Create engine in RAM - + from db.models import Reminder # noqa: F401, import for Base - Base.metadata.create_all(bind=engine) # Creating db @@ -52,3 +53,14 @@ def override_get_db(): yield c # Creating client for an action app.dependency_overrides.clear() # Switching back to database + + +@pytest.fixture +def reminder_payload(): + return { + "title": 'Laundry', + "description": "Remember about laundry", + "due_to": "24-12-2026 12:00", + "email": "test@example.com", + "alert_type": "minutes" + } diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 1446003..b8474d8 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,19 +1,130 @@ from fastapi.testclient import TestClient -from app.main import get_db +import pytest -def test_create_reminder(client: TestClient): +@pytest.mark.crud +def test_create_reminder(client: TestClient, reminder_payload): + """ + Creating reminder, id should appera in data + """ + response = client.post("/create", json=reminder_payload) + + assert response.status_code == 200 + data = response.json() + assert data['title'] == "Laundry" + assert "id" in data + + +@pytest.mark.crud +def test_get_reminder(client: TestClient, reminder_payload): + """ + Get one reminder, values should be matching + """ + response = client.post("/create", json=reminder_payload) + assert response.status_code == 200 + data = response.json() + + response_get = client.get(f"/reminders/{data.get('id')}") + + assert response_get.status_code == 200 + data_get = response_get.json() + assert data_get['title'] == reminder_payload['title'] + assert data_get['description'] == reminder_payload['description'] + assert data_get['email'] == reminder_payload['email'] + assert data_get['alert_type'] == reminder_payload['alert_type'] + + +@pytest.mark.crud +def test_delete_reminder_and_error(client: TestClient, reminder_payload): + """ + Tests deleting all reminders. + Aditionally checks status code for deleting non existing reminder + """ + response = client.post("/create", json=reminder_payload) + assert response.status_code == 200 + data = response.json() + response_delete = client.delete(f"/reminders/{data['id']}") + assert response_delete.status_code == 200 + response_delete_2 = client.delete(f"/reminders/{data['id']}") + assert response_delete_2.status_code == 404 + + +@pytest.mark.crud +def test_put(client: TestClient, reminder_payload): + """ + Tests PUT for Reminder, should return status code 200 and new values + """ + response = client.post("/create", json=reminder_payload) + assert response.status_code == 200 + data = response.json() + + payload_put = { + "title": "Clean windows", + 'description': "I've changed my mind" + } + + response_put = client.put(f"/reminders/{data['id']}", json=payload_put) + assert response_put.status_code == 200 + + response_get = client.get(f"/reminders/{data['id']}") + data_get = response_get.json() + + assert data_get['title'] == payload_put["title"] + assert data_get['description'] == payload_put['description'] + assert data_get['email'] == reminder_payload['email'] + assert data_get['alert_type'] == reminder_payload['alert_type'] + + +@pytest.mark.validation +def test_wrong_date(client: TestClient): + """ + Tests providing wrong date format, should return status code 422 and ValueError + """ payload = { "title": 'Laundry', "description": "Remember about laundry", - "due_to": "24-12-2026 12:00", - "email": "test@example", + "due_to": "2026-12-42T12:00Z", + "email": "test@example.com", "alert_type": "minutes" } - response = client.post("/create", json=payload) + assert response.status_code == 422 + assert "Field should be filled in dd-mm-yyyy hh:mm format" in response.text + +@pytest.mark.crud +def test_all_reminders(client: TestClient, reminder_payload): + """ + Tests if getting all reminders works properly + """ + response = client.post("/create", json=reminder_payload) assert response.status_code == 200 - data = response.json() - assert data['title'] == "Laundry" - assert "id" in data + response2 = client.post("/create", json=reminder_payload) + assert response2.status_code == 200 + + response_get_all = client.get("/reminders") + assert response_get_all.status_code == 200 + assert len(response_get_all.json()) == 2 + + +@pytest.mark.crud +def test_delete_all(client: TestClient, reminder_payload): + """ + Test of deleting all reminders, should return empty list after. + Message returned if trying to delete empty list. + """ + response = client.post("/create", json=reminder_payload) + assert response.status_code == 200 + response2 = client.post("/create", json=reminder_payload) + assert response2.status_code == 200 + + response_delete = client.delete("/reminders") + assert response_delete.status_code == 200 + + response_get_all = client.get("/reminders") + assert response_get_all.status_code == 200 + assert response_get_all.json() == [] + + response_delete = client.delete("/reminders") + assert response_delete.status_code == 200 + assert response_delete.json() == {'message': 'No reminders to delete'} diff --git a/backend/tests/test_crud.py b/backend/tests/test_crud.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/tests/test_schemas.py b/backend/tests/test_schemas.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/tests/test_tasks.py b/backend/tests/test_tasks.py deleted file mode 100644 index e69de29..0000000