Repository: resulyrt93/pytest-sqlalchemy-mock (language: python)

Issue:
- bump version to 0.1.5
- add assocation table support
- add new tests for m2m relationship
- upgrade isort pre-commit hook version


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..06e4886
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[report]
+exclude_lines =
+    pragma: no cover
+    if TYPE_CHECKING:
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..0885295
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,28 @@
+name: Deploy
+on:
+  push:
+    tags:
+      - 'v*'
+jobs:
+  build-n-publish:
+    name: Build and publish Python 🐍 distributions 📦 to PyPI
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@master
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
+          cache: 'pip'
+          cache-dependency-path: '**/requirements-dev.txt'
+      - name: Install pypa/build
+        run: python -m pip install build
+      - name: Build a binary wheel and a source tarball
+        run: python -m build --sdist --wheel --outdir dist/ .
+      - name: Deploy package
+        if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          verbose: true
+          skip_existing: true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index eafbbe9..55400ec 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
         exclude: '.*\.pth$'
       - id: debug-statements
   - repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
+    rev: 5.12.0
     hooks:
       - id: isort
   - repo: https://github.com/PyCQA/flake8
diff --git a/README.md b/README.md
index 61cdd99..fc5f2de 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,75 @@
-# pytest-sqlalchemy-mock
+# pytest-sqlalchemy-mock  👋
 [![PyPI version](https://badge.fury.io/py/pytest-sqlalchemy-mock.svg)](https://badge.fury.io/py/pytest-sqlalchemy-mock)
+[![codecov](https://codecov.io/gh/resulyrt93/pytest-sqlalchemy-mock/branch/dev/graph/badge.svg?token=RUQ4DN3CH9)](https://codecov.io/gh/resulyrt93/pytest-sqlalchemy-mock)
+[![CI](https://github.com/resulyrt93/pytest-sqlalchemy-mock/actions/workflows/tests.yaml/badge.svg?branch=dev)](https://github.com/resulyrt93/pytest-sqlalchemy-mock/actions/workflows/tests.yaml)
+[![Supported Python Version](https://img.shields.io/pypi/pyversions/pytest-sqlalchemy-mock)](https://github.com/resulyrt93/pytest-sqlalchemy-mock)
+<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
 
-pytest plugin to mock sqlalchemy models
+This plugin provides pytest fixtures to create an in-memory DB instance on tests and dump your raw test data.
+
+## Installation
+```
+pip install pytest-sqlalchemy-mock
+```
+
+## Usage
+Let's assume you have a SQLAlchemy declarative base and some models with it.
+
+**models.py**
+```python
+from sqlalchemy import Column, Integer, String
+from sqlalchemy.orm import declarative_base
+
+Base = declarative_base()
+
+
+class User(Base):
+    __tablename__ = "user"
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String)
+```
+Firstly SQLAlchemy base class which is used for declare models should be passed with `sqlalchemy_declarative_base` fixture in `conftest.py`
+
+**conftest.py**
+```python
+@pytest.fixture(scope="function")
+def sqlalchemy_declarative_base():
+    return Base
+```
+Then in test functions you can use `mocked_session` fixture to make query in mocked DB.
+
+**test_user_table.py**
+```python
+def test_mocked_session_user_table(mocked_session):
+    user_data = mocked_session.execute("SELECT * from user;").all()
+    assert user_data == []
+```
+Also, you can dump your mock data to DB before start testing via `sqlalchemy_mock_config` fixture like following.
+
+**conftest.py**
+```python
+@pytest.fixture(scope="function")
+def sqlalchemy_mock_config():
+    return [("user", [
+        {
+            "id": 1,
+            "name": "Kevin"
+        },
+        {
+            "id": 2,
+            "name": "Dwight"
+        }
+    ])]
+```
+**test_user_table.py**
+```python
+def test_mocked_session_user_class(mocked_session):
+    user = mocked_session.query(User).filter_by(id=2).first()
+    assert user.name == "Dwight"
+```
+
+## Upcoming Features
+* Mock with decorator for specific DB states for specific cases.
+* Support to load data from `.json` and `.csv`
+* Async SQLAlchemy support
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 780ae47..51432d5 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,5 @@
-sqlalchemy
+sqlalchemy==2.0.6
 flake8
 pytest
+pytest-cov
 pre-commit
-sqlalchemy
diff --git a/setup.py b/setup.py
index 340ffd5..86118d9 100644
--- a/setup.py
+++ b/setup.py
@@ -8,22 +8,28 @@ def read(fname):
 
 
 setup(
-    name='pytest-sqlalchemy-mock',
-    license='MIT',
-    description='pytest sqlalchemy plugin for mock',
-    # long_description=read("README.rst"),
-    version='0.1.0',
-    author='Resul Yurttakalan',
-    author_email='resulyrt93@gmail.com',
-    url='https://github.com/resulyrt93/pytest-sqlalchemy-mock',
-    py_modules=['pytest_sqlalchemy_mock'],
-    entry_points={'pytest11': ['sqlalchemy = pytest_sqlalchemy_mock']},
-    install_requires=['pytest>=2.0', 'sqlalchemy'],
+    name="pytest-sqlalchemy-mock",
+    license="MIT",
+    description="pytest sqlalchemy plugin for mock",
+    long_description=read("README.md"),
+    long_description_content_type="text/markdown",
+    version="0.1.5",
+    author="Resul Yurttakalan",
+    author_email="resulyrt93@gmail.com",
+    url="https://github.com/resulyrt93/pytest-sqlalchemy-mock",
+    packages=["pytest_sqlalchemy_mock"],
+    entry_points={
+        "pytest11": ["pytest_sqlalchemy_mock = pytest_sqlalchemy_mock.base"]
+    },
+    install_requires=["pytest>=2.0", "sqlalchemy"],
     classifiers=[
-        'Framework :: Pytest',
-        'Development Status :: 3 - Alpha',
-        'Topic :: Software Development :: Testing',
-        'Intended Audience :: Developers',
-        'Operating System :: OS Independent',
-    ]
+        "Framework :: Pytest",
+        "Development Status :: 3 - Alpha",
+        "Topic :: Software Development :: Testing",
+        "Intended Audience :: Developers",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+    ],
 )


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index c17d6dd..13f6d3b 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,21 +1,27 @@
-name: Python package
+name: CI
 
-on: [ push, pull_request ]
+on:
+  push:
+    branches:
+      - dev
+  pull_request:
+    branches:
+      - dev
 
 jobs:
-  build:
-
+  test:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ "3.7", "3.8", "3.9", "3.10" ]
-
+        python-version: [ "3.8", "3.9", "3.10" ]
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
+          cache: 'pip'
+          cache-dependency-path: '**/requirements-dev.txt'
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
@@ -25,4 +31,6 @@ jobs:
           pre-commit run --all-files --show-diff-on-failure
       - name: Test with pytest
         run: |
-          pytest tests
+          pytest --cov pytest_sqlalchemy_mock --cov-report=term-missing
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v3
diff --git a/pytest_sqlalchemy_mock.py b/pytest_sqlalchemy_mock.py
deleted file mode 100644
index e1dc3b5..0000000
--- a/pytest_sqlalchemy_mock.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from typing import TYPE_CHECKING
-
-import pytest
-from sqlalchemy import create_engine
-from sqlalchemy.orm import sessionmaker
-
-if TYPE_CHECKING:
-    from sqlalchemy.orm import Session
-
-
-@pytest.fixture(scope="session")
-def connection_url():
-    return "sqlite:///:memory:"
-
-
-@pytest.fixture(scope="function")
-def engine(connection_url):
-    return create_engine(connection_url)
-
-
-@pytest.fixture(scope="function")
-def connection(engine):
-    return engine.connect()
-
-
-@pytest.fixture(scope="function")
-def session(connection):
-    session: Session = sessionmaker()(bind=connection)
-    yield session
-    session.close()
diff --git a/pytest_sqlalchemy_mock/__init__.py b/pytest_sqlalchemy_mock/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pytest_sqlalchemy_mock/base.py b/pytest_sqlalchemy_mock/base.py
new file mode 100644
index 0000000..d684e23
--- /dev/null
+++ b/pytest_sqlalchemy_mock/base.py
@@ -0,0 +1,59 @@
+from typing import TYPE_CHECKING
+
+import pytest
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from .model_mocker import ModelMocker
+
+if TYPE_CHECKING:
+    from sqlalchemy.orm import Session
+
+
+@pytest.fixture(scope="session")
+def connection_url():
+    return "sqlite:///:memory:"
+
+
+@pytest.fixture(scope="function")
+def engine(connection_url):
+    return create_engine(connection_url)
+
+
+@pytest.fixture(scope="function")
+def sqlalchemy_declarative_base():
+    return
+
+
+@pytest.fixture(scope="function")
+def sqlalchemy_mock_config():
+    return
+
+
+@pytest.fixture(scope="function")
+def connection(engine, sqlalchemy_declarative_base):
+    if sqlalchemy_declarative_base:
+        sqlalchemy_declarative_base.metadata.create_all(engine)
+    return engine.connect()
+
+
+@pytest.fixture(scope="function")
+def session(connection):
+    session: Session = sessionmaker()(bind=connection)
+    yield session
+    session.close()
+
+
+@pytest.fixture(scope="function")
+def mocked_session(
+    connection, sqlalchemy_declarative_base, sqlalchemy_mock_config
+):
+    session: Session = sessionmaker()(bind=connection)
+
+    if sqlalchemy_declarative_base and sqlalchemy_mock_config:
+        ModelMocker(
+            session, sqlalchemy_declarative_base, sqlalchemy_mock_config
+        ).create_all()
+
+    yield session
+    session.close()
diff --git a/pytest_sqlalchemy_mock/model_mocker.py b/pytest_sqlalchemy_mock/model_mocker.py
new file mode 100644
index 0000000..097d2f8
--- /dev/null
+++ b/pytest_sqlalchemy_mock/model_mocker.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import List, Tuple
+
+    from sqlalchemy import Table
+    from sqlalchemy.orm import Session
+
+    MOCK_CONFIG_TYPE = List[Tuple[str, List]]
+
+
+class ModelMocker:
+    _base = None
+    _mock_config: MOCK_CONFIG_TYPE = None
+
+    def __init__(self, session: Session, base, mock_config: MOCK_CONFIG_TYPE):
+        self._session: Session = session
+        self._base = base
+        self._mock_config = mock_config
+
+    def get_table_by_name(self, table_name: str) -> Table:
+        return self._base.metadata.tables.get(table_name)
+
+    def create_all(self):
+        for model_config in self._mock_config:
+            table_name, data = model_config
+            table = self.get_table_by_name(table_name)
+
+            if table is not None:
+                for datum in data:
+                    instance = table.insert().values(**datum)
+                    self._session.execute(instance)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1cd8595..fc5c081 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,1 +1,18 @@
-from pytest_sqlalchemy_mock import *
+# TODO use pytester for testing
+from pytest_sqlalchemy_mock.base import *
+from tests.data import MockData
+from tests.db import Base
+
+
+@pytest.fixture(scope="function")
+def sqlalchemy_declarative_base():
+    return Base
+
+
+@pytest.fixture(scope="function")
+def sqlalchemy_mock_config():
+    return [
+        ("user", MockData.USER_DATA),
+        ("department", MockData.DEPARTMENT_DATA),
+        ("user_department", MockData.USER_DEPARTMENT_DATA),
+    ]
diff --git a/tests/data.py b/tests/data.py
new file mode 100644
index 0000000..0408730
--- /dev/null
+++ b/tests/data.py
@@ -0,0 +1,44 @@
+from datetime import datetime
+
+
+class MockData:
+    USER_DATA = [
+        {
+            "id": 1,
+            "name": "Kevin",
+            "surname": "Malone",
+            "is_admin": False,
+            "city": "NY",
+            "join_date": datetime(2011, 7, 27, 16, 2, 8),
+        },
+        {
+            "id": 2,
+            "name": "Dwight",
+            "surname": "Schrute",
+            "is_admin": True,
+            "city": "PA",
+            "join_date": datetime(2009, 11, 20, 21, 3, 12),
+        },
+    ]
+
+    DEPARTMENT_DATA = [
+        {
+            "id": 1,
+            "name": "Accounting",
+        },
+        {
+            "id": 2,
+            "name": "Sales",
+        },
+    ]
+
+    USER_DEPARTMENT_DATA = [
+        {
+            "user_id": 1,
+            "department_id": 1,
+        },
+        {
+            "user_id": 2,
+            "department_id": 2,
+        },
+    ]
diff --git a/tests/db.py b/tests/db.py
new file mode 100644
index 0000000..f84544e
--- /dev/null
+++ b/tests/db.py
@@ -0,0 +1,43 @@
+from typing import List
+
+from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String,
+                        func)
+from sqlalchemy.orm import Mapped, declarative_base, relationship
+from sqlalchemy.testing.schema import Table
+
+Base = declarative_base()
+
+
+user_department_association_table = Table(
+    "user_department",
+    Base.metadata,
+    Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
+    Column(
+        "department_id", Integer, ForeignKey("department.id"), primary_key=True
+    ),
+)
+
+
+class User(Base):
+    __tablename__ = "user"
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String)
+    surname = Column(String)
+    is_admin = Column(Boolean, default=False)
+    city = Column(String)
+    join_date = Column(DateTime(timezone=True), server_default=func.now())
+    departments: Mapped[List["Department"]] = relationship(
+        secondary=user_department_association_table, back_populates="users"
+    )
+
+
+class Department(Base):
+    __tablename__ = "department"
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String)
+    users: Mapped[List[User]] = relationship(
+        secondary=user_department_association_table,
+        back_populates="departments",
+    )
diff --git a/tests/test_pytest_sqlalchemy_mock.py b/tests/test_pytest_sqlalchemy_mock.py
index c14bb1f..8992fe8 100644
--- a/tests/test_pytest_sqlalchemy_mock.py
+++ b/tests/test_pytest_sqlalchemy_mock.py
@@ -1,2 +1,45 @@
+from sqlalchemy import text
+
+from .data import MockData
+from .db import Department, User
+
+
 def test_get_session(session):
-    assert session.execute("SELECT 5").scalar() == 5
+    assert session.execute(text("SELECT 5")).scalar() == 5
+
+
+def test_session_user_table(session):
+    assert session.execute(text("SELECT count(*) from user")).scalar() == 0
+
+
+def test_session_query_for_assocation_table(session):
+    assert (
+        session.execute(text("SELECT count(*) from user_department")).scalar()
+        == 0
+    )
+
+
+def test_mocked_session_user_table(mocked_session):
+    user_data = mocked_session.execute(text("SELECT * from user;")).first()
+    raw_data = MockData.USER_DATA[0]
+    assert user_data[0] == raw_data["id"]
+    assert user_data[1] == raw_data["name"]
+    assert user_data[2] == raw_data["surname"]
+    assert user_data[3] == raw_data["is_admin"]
+    assert user_data[4] == raw_data["city"]
+
+
+def test_mocked_session_user_model(mocked_session):
+    user = mocked_session.query(User).filter_by(id=2).first()
+    raw_data = MockData.USER_DATA[1]
+    assert user.name == raw_data["name"]
+    assert user.is_admin == raw_data["is_admin"]
+    assert user.departments[0].name == "Sales"
+
+
+def test_mocked_session_department_model(mocked_session):
+    department = mocked_session.query(Department).filter_by(id=1).first()
+    raw_data = MockData.DEPARTMENT_DATA[0]
+    assert department.name == raw_data["name"]
+    assert len(department.users) == 1
+    assert department.users[0].name == "Kevin"