Set up tests

This commit is contained in:
Vladyslav Fedoriuk 2023-07-29 00:17:38 +02:00
parent e4e0ab47f7
commit a0310ea3b1
10 changed files with 160 additions and 21 deletions

View File

@ -32,4 +32,4 @@ jobs:
python -m pyupgrade --py311-plus
- name: Lint with pyproject-fmt
run: |
python -m pyproject_fmt --stdout --check
python -m pyproject_fmt --stdout --check --indent=4

View File

@ -37,6 +37,7 @@ repos:
rev: "0.11.1"
hooks:
- id: pyproject-fmt
args: ["--indent=4"]
- repo: https://github.com/jorisroovers/gitlint
rev: 'v0.19.1'
hooks:

View File

@ -1 +1,61 @@
"""The application-level conftest."""
import asyncio
from collections.abc import AsyncGenerator
from typing import Literal
import pytest
from pytest_mock import MockerFixture
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import async_session_maker
@pytest.fixture
def anyio_backend() -> Literal["asyncio"]:
"""Use asyncio as the async backend."""
return "asyncio"
@pytest.fixture(autouse=True)
def test_db(mocker: MockerFixture) -> None:
"""Use the in-memory database for tests."""
mocker.patch("app.database.DB_PATH", "sqlite+aiosqlite:///")
@pytest.fixture(scope="session")
def event_loop(
request: pytest.FixtureRequest,
) -> AsyncGenerator[asyncio.AbstractEventLoop, None]:
"""
Create an instance of the default event loop for a session.
An event loop is destroyed at the end of the test session.
https://docs.pytest.org/en/6.2.x/fixture.html#fixture-scopes
"""
loop = asyncio.get_event_loop_policy().get_event_loop()
try:
yield loop
finally:
loop.close()
@pytest.fixture
async def test_db_session() -> AsyncGenerator[AsyncSession, None]:
"""Use the in-memory database for tests."""
from app.database import Base, engine
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
try:
async with async_session_maker() as session:
yield session
finally:
await session.flush()
await session.rollback()
await session.close()
finally:
# for AsyncEngine created in function scope, close and
# clean-up pooled connections
await engine.dispose()

View File

@ -22,6 +22,7 @@ from typing import Final
from sqlalchemy import ForeignKey, String
from sqlalchemy.ext.asyncio import (
AsyncAttrs,
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
@ -32,9 +33,11 @@ DB_PATH: Final[PurePath] = PurePath(__file__).parent.parent / "db.sqlite3"
SQLALCHEMY_DATABASE_URL: Final[str] = f"sqlite+aiosqlite:///{DB_PATH}"
engine = create_async_engine(SQLALCHEMY_DATABASE_URL)
engine: Final[AsyncEngine] = create_async_engine(SQLALCHEMY_DATABASE_URL)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
async_session_maker: Final[async_sessionmaker[AsyncSession]] = async_sessionmaker(
engine, expire_on_commit=False, autoflush=False, autocommit=False
)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
@ -65,7 +68,7 @@ class Dependency(Base):
__tablename__ = "dependency"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
repos: Mapped[list["Repo"]] = relationship(
"Repo", secondary="repo_dependency", back_populates="dependencies"
)

12
app/factories.py Normal file
View File

@ -0,0 +1,12 @@
"""Factories for creating models for testing."""
from polyfactory.factories.pydantic_factory import ModelFactory
from polyfactory.pytest_plugin import register_fixture
from app.models import RepoCreateData
@register_fixture
class RepoCreateDataFactory(ModelFactory[RepoCreateData]):
"""Factory for creating RepoCreateData."""
__model__ = RepoCreateData

19
app/models.py Normal file
View File

@ -0,0 +1,19 @@
"""Module contains the models for the application."""
from typing import NewType
from pydantic import AnyUrl, BaseModel
RepoId = NewType("RepoId", int)
DependencyId = NewType("DependencyId", int)
class RepoCreateData(BaseModel):
"""A repository that is being tracked."""
url: AnyUrl
class DependencyCreateData(BaseModel):
"""A dependency of a repository."""
name: str

View File

@ -0,0 +1,22 @@
"""Test the operations on the database models."""
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app import database
from app.factories import RepoCreateDataFactory
pytestmark = pytest.mark.anyio
async def test_create_repo_no_dependencies(
test_db_session: AsyncSession, repo_create_data_factory: RepoCreateDataFactory
) -> None:
"""Test creating a repo."""
repo_create_data = repo_create_data_factory.build()
repo = database.Repo(url=str(repo_create_data.url))
test_db_session.add(repo)
await test_db_session.commit()
await test_db_session.refresh(repo)
assert repo.id is not None
assert repo.url == str(repo_create_data.url)
assert (await repo.awaitable_attrs.dependencies) == []

View File

@ -1,15 +1,15 @@
"""Add Repo, Dependency, and RepoDependency tables
Revision ID: 7ee6dc0ae743
Revision ID: d8fc955c639b
Revises:
Create Date: 2023-07-28 22:41:31.438931
Create Date: 2023-07-28 23:41:00.169286
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "7ee6dc0ae743"
revision = "d8fc955c639b"
down_revision = None
branch_labels = None
depends_on = None
@ -22,6 +22,7 @@ def upgrade() -> None:
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"repo",

View File

@ -7,27 +7,30 @@ authors = [
]
requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"aiosqlite",
"alembic",
"fastapi[all]",
"sqlalchemy[asyncio]",
"aiosqlite",
"alembic",
"fastapi[all]",
"sqlalchemy[asyncio]",
]
[project.optional-dependencies]
dev = [
"black",
"isort",
"pip-tools",
"pre-commit",
"pyproject-fmt",
"pyupgrade",
"ruff",
"black",
"isort",
"pip-tools",
"pre-commit",
"pyproject-fmt",
"pyupgrade",
"ruff",
]
test = [
"pytest",
"polyfactory",
"pytest",
"pytest-anyio",
"pytest-mock",
]
[tool.setuptools]
@ -93,6 +96,7 @@ extend-select = [
# Ignore missing docstrings in migrations and alembic files
"**/migrations/*.py" = ["D"]
"**/migrations/env.py" = ["ERA001"]
"**/tests/*.py" = ["S101"]
[tool.ruff.pydocstyle]
convention = "numpy"

View File

@ -13,6 +13,7 @@ annotated-types==0.5.0
anyio==3.7.1
# via
# httpcore
# pytest-anyio
# starlette
# watchfiles
certifi==2023.7.22
@ -25,6 +26,8 @@ dnspython==2.4.1
# via email-validator
email-validator==2.0.0.post2
# via fastapi
faker==19.2.0
# via polyfactory
fastapi[all]==0.100.0
# via awesome-fastapi-projects (pyproject.toml)
greenlet==2.0.2
@ -62,6 +65,8 @@ packaging==23.1
# via pytest
pluggy==1.2.0
# via pytest
polyfactory==2.7.0
# via awesome-fastapi-projects (pyproject.toml)
pydantic==2.1.1
# via
# fastapi
@ -74,7 +79,16 @@ pydantic-extra-types==2.0.0
pydantic-settings==2.0.2
# via fastapi
pytest==7.4.0
# via
# awesome-fastapi-projects (pyproject.toml)
# pytest-anyio
# pytest-mock
pytest-anyio==0.0.0
# via awesome-fastapi-projects (pyproject.toml)
pytest-mock==3.11.1
# via awesome-fastapi-projects (pyproject.toml)
python-dateutil==2.8.2
# via faker
python-dotenv==1.0.0
# via
# pydantic-settings
@ -85,6 +99,8 @@ pyyaml==6.0.1
# via
# fastapi
# uvicorn
six==1.16.0
# via python-dateutil
sniffio==1.3.0
# via
# anyio
@ -100,6 +116,7 @@ typing-extensions==4.7.1
# via
# alembic
# fastapi
# polyfactory
# pydantic
# pydantic-core
# sqlalchemy