FIXDOCKER и typehint в тестах

develop
Сергей Ванюшкин 2024-01-30 23:11:40 +03:00
parent f61cb3a2ee
commit ead24d9f28
13 changed files with 130 additions and 77 deletions

View File

@ -1,15 +1,13 @@
FROM python:3.10-slim
RUN mkdir /fastfood
WORKDIR /fastfood
COPY . .
RUN pip install poetry
RUN poetry config virtualenvs.create false
RUN poetry install
RUN mkdir -p /usr/src/fastfood
RUN chmod a+x scripts/*.sh
WORKDIR /usr/src/fastfood
COPY . .
RUN poetry install

View File

@ -98,7 +98,11 @@ Fastapi веб приложение реализующее api для общеп
где <db_name> и <db_user> соответвтовали POSTGRES_DB и POSTGRES_USER в файле `.env`
Создайте и запустите образы
> `$ docker-compose up -d --build`
Запуск FAstAPI приложения
> `$ docker-compose -f compose_app.yml up -d`
Запуск тестов
> `$ docker-compose -f compose_test.yml up`
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>

45
compose_app.yml Normal file
View File

@ -0,0 +1,45 @@
version: "3.8"
services:
db:
container_name: pgdb
image: postgres:15.1-alpine
env_file:
- .env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- 6432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
app:
container_name: fastfood_app
build:
context: .
env_file:
- .env
ports:
- 8000:8000
depends_on:
db:
condition: service_healthy
restart: always
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server'

45
compose_test.yml Normal file
View File

@ -0,0 +1,45 @@
version: "3.8"
services:
db:
container_name: pgdb_test
image: postgres:15.1-alpine
env_file:
- .env
environment:
POSTGRES_DB: "${POSTGRES_DB}_test"
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- 6432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}_test"]
interval: 10s
timeout: 5s
retries: 5
app:
container_name: fastfood_app_test
build:
context: .
env_file:
- .env
ports:
- 8000:8000
depends_on:
db:
condition: service_healthy
restart: always
command: /bin/bash -c 'poetry run pytest -vv'

View File

@ -1,44 +0,0 @@
version: "3.8"
services:
db:
image: postgres:15.1-alpine
env_file:
- .env
container_name: pgdatabase
ports:
- 6432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ./scripts/db_prepare.sql:/docker-entrypoint-initdb.d/db_prepare.sql
app:
build:
context: .
container_name: fastfood_app
env_file:
- .env
command: ["/fastfood/scripts/migrate_and_run.sh"]
ports:
- 8000:8000
depends_on:
db:
condition: service_healthy
restart: always
tests:
build:
context: .
container_name: tests
env_file:
- .env
depends_on:
db:
condition: service_healthy
app:
condition: service_started
command: ["/fastfood/scripts/testing.sh"]

View File

@ -1,5 +1,5 @@
DB_HOST=db
DB_PORT=5432
POSTGRES_USER=postges
POSTGRES_PASSWORD=postgres
POSTGRES_DB=fastfood_db
POSTGRES_USER=testuser
POSTGRES_PASSWORD=test
POSTGRES_DB=fastfood_db

View File

@ -4,9 +4,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DB_HOST: str = "db"
DB_PORT: int = 5432
POSTGRES_DB: str = "fastfod_db"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_USER: str = "postgres"
POSTGRES_DB: str = "fastfood_db"
POSTGRES_PASSWORD: str = "test"
POSTGRES_USER: str = "testuser"
POSTGRES_DB_TEST: str = "fastfood_db_test"
@property
def DATABASE_URL_asyncpg(self):
@ -27,7 +28,7 @@ class Settings(BaseSettings):
return (
"postgresql+asyncpg://"
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}_test"
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}"
)
model_config = SettingsConfigDict(env_file=".env")

View File

@ -1 +0,0 @@
CREATE DATABASE fastfood_db_test WITH OWNER postgres;

View File

@ -1,5 +0,0 @@
#!/bin/bash
#
# Тут можно выполнить миграции или дополнительные перед запуском приложения
#
poetry run python manage.py --run-test-server

View File

@ -1,2 +0,0 @@
#!/bin/bash
poetry run pytest -vv

View File

@ -21,7 +21,7 @@ async_session_maker = async_sessionmaker(
)
@pytest.fixture(scope="session")
@pytest.fixture(scope="session", autouse=True)
def event_loop():
try:
loop = asyncio.get_event_loop()
@ -32,7 +32,7 @@ def event_loop():
@pytest_asyncio.fixture(scope="function", autouse=True)
async def db_init():
async def db_init(event_loop):
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
@ -47,7 +47,7 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
@pytest.fixture(scope="session")
def app() -> Generator[FastAPI, None, None]:
def app(event_loop) -> Generator[FastAPI, None, None]:
app: FastAPI = create_app()
app.dependency_overrides[get_async_session] = get_test_session
yield app
@ -63,6 +63,6 @@ async def client(app) -> AsyncGenerator[AsyncClient, None]:
@pytest_asyncio.fixture(scope="function")
async def asession() -> AsyncGenerator[AsyncSession, None]:
async def asession(event_loop) -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session

View File

@ -47,7 +47,11 @@ class TestBaseCrud:
return response.status_code, response.json()
@staticmethod
async def get(ac: AsyncClient, menu: dict, submenu: dict) -> Tuple[int, dict]:
async def get(
ac: AsyncClient,
menu: dict,
submenu: dict,
) -> Tuple[int, dict]:
"""Получение меню по id"""
response: Response = await ac.get(
f"/{menu.get('id')}/submenus/{submenu.get('id')}",
@ -55,7 +59,11 @@ class TestBaseCrud:
return response.status_code, response.json()
@staticmethod
async def write(ac: AsyncClient, menu: dict, submenu: dict) -> Tuple[int, dict]:
async def write(
ac: AsyncClient,
menu: dict,
submenu: dict,
) -> Tuple[int, dict]:
"""создания меню"""
response: Response = await ac.post(
f"/{menu.get('id')}/submenus/",

View File

@ -48,7 +48,7 @@ async def test_menu(asession: AsyncSession) -> None:
async def test_submenu(asession: AsyncSession) -> None:
async with asession:
# Создаем меню напрямую
menu: Menu = Menu(title="SomeMenu", description="SomeDescription")
menu = Menu(title="SomeMenu", description="SomeDescription")
asession.add(menu)
await asession.commit()
await asession.refresh(menu)
@ -69,14 +69,18 @@ async def test_submenu(asession: AsyncSession) -> None:
# Проверяем подменю
req_submenu = await SubMenuCrud.get_submenu_item(
menu_id, submenu.id, asession,
menu_id,
submenu.id,
asession,
)
assert submenu == req_submenu
# Обновляем меню
submenu.title = "UpdatedSubmenu"
req_submenu = await SubMenuCrud.update_submenu_item(
submenu_id, menubaseschema.model_validate(submenu), asession,
submenu_id,
menubaseschema.model_validate(submenu),
asession,
)
assert submenu == req_submenu.scalar_one_or_none()