├── tests ├── __init__.py ├── conftest.py ├── data.py ├── db.py └── test_pytest_sqlalchemy_mock.py ├── src └── pytest_sqlalchemy_mock │ ├── __init__.py │ ├── model_mocker.py │ └── base.py ├── .coveragerc ├── .flake8 ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── deploy.yaml │ └── tests.yaml ├── LICENSE ├── pyproject.toml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pytest_sqlalchemy_mock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | if TYPE_CHECKING: 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = 4 | .git, 5 | .github, 6 | .venv, 7 | build, 8 | dist, 9 | tests/conftest.py 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # TODO use pytester for testing 2 | from pytest_sqlalchemy_mock.base import * 3 | from tests.data import MockData 4 | from tests.db import Base 5 | 6 | 7 | @pytest.fixture(scope="function") 8 | def sqlalchemy_declarative_base(): 9 | return Base 10 | 11 | 12 | @pytest.fixture(scope="function") 13 | def sqlalchemy_mock_config(): 14 | return [ 15 | ("user", MockData.USER_DATA), 16 | ("department", MockData.DEPARTMENT_DATA), 17 | ("user_department", MockData.USER_DEPARTMENT_DATA), 18 | ] 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # To install the git pre-commit hook run: 2 | # pre-commit install 3 | # To update the pre-commit hooks run: 4 | # pre-commit install-hooks 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | exclude: '.*\.pth$' 12 | - id: debug-statements 13 | - repo: https://github.com/PyCQA/isort 14 | rev: 5.13.2 15 | hooks: 16 | - id: isort 17 | - repo: https://github.com/PyCQA/flake8 18 | rev: 7.0.0 19 | hooks: 20 | - id: flake8 21 | - repo: https://github.com/psf/black 22 | rev: 23.12.1 23 | hooks: 24 | - id: black 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build-n-publish: 8 | name: Build and publish Python 🐍 distributions 📦 to PyPI 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Set up Python 3.10 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.10" 16 | cache: 'pip' 17 | cache-dependency-path: '**/requirements-dev.txt' 18 | - name: Install pypa/build 19 | run: python -m pip install build 20 | - name: Build a binary wheel and a source tarball 21 | run: python -m build --sdist --wheel --outdir dist/ . 22 | - name: Deploy package 23 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | with: 26 | password: ${{ secrets.PYPI_API_TOKEN }} 27 | verbose: true 28 | skip_existing: true 29 | -------------------------------------------------------------------------------- /tests/data.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class MockData: 5 | USER_DATA = [ 6 | { 7 | "id": 1, 8 | "name": "Kevin", 9 | "surname": "Malone", 10 | "is_admin": False, 11 | "city": "NY", 12 | "join_date": datetime(2011, 7, 27, 16, 2, 8), 13 | }, 14 | { 15 | "id": 2, 16 | "name": "Dwight", 17 | "surname": "Schrute", 18 | "is_admin": True, 19 | "city": "PA", 20 | "join_date": datetime(2009, 11, 20, 21, 3, 12), 21 | }, 22 | ] 23 | 24 | DEPARTMENT_DATA = [ 25 | { 26 | "id": 1, 27 | "name": "Accounting", 28 | }, 29 | { 30 | "id": 2, 31 | "name": "Sales", 32 | }, 33 | ] 34 | 35 | USER_DEPARTMENT_DATA = [ 36 | { 37 | "user_id": 1, 38 | "department_id": 1, 39 | }, 40 | { 41 | "user_id": 2, 42 | "department_id": 2, 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | - 'feature/**' 8 | pull_request: 9 | branches: 10 | - dev 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9", "3.10", "3.11", "3.12" ] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install --no-cache-dir ".[dev]" 28 | pip uninstall -y pytest-sqlalchemy-mock 29 | - name: Run pre commit hooks 30 | run: | 31 | pre-commit run --all-files --show-diff-on-failure 32 | - name: Test with pytest 33 | run: | 34 | pytest --cov pytest_sqlalchemy_mock --cov-report=term-missing -s -vv tests/ 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v3 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Resul Yurttakalan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pytest_sqlalchemy_mock/model_mocker.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from typing import ( 7 | List, 8 | Tuple, 9 | ) 10 | 11 | from sqlalchemy import Table 12 | from sqlalchemy.orm import Session 13 | 14 | MOCK_CONFIG_TYPE = List[Tuple[str, List]] 15 | 16 | 17 | class ModelMocker: 18 | _base = None 19 | _mock_config: MOCK_CONFIG_TYPE = None 20 | 21 | def __init__(self, session: Session, base, mock_config: MOCK_CONFIG_TYPE): 22 | self._session: Session = session 23 | self._base = base 24 | self._mock_config = mock_config 25 | 26 | def get_table_by_name(self, table_name: str) -> Table: 27 | return self._base.metadata.tables.get(table_name) 28 | 29 | def create_all(self): 30 | for model_config in self._mock_config: 31 | table_name, data = model_config 32 | table = self.get_table_by_name(table_name) 33 | 34 | if table is not None: 35 | for datum in data: 36 | instance = table.insert().values(**datum) 37 | self._session.execute(instance) 38 | -------------------------------------------------------------------------------- /tests/db.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import ( 4 | Boolean, 5 | Column, 6 | DateTime, 7 | ForeignKey, 8 | Integer, 9 | String, 10 | func, 11 | ) 12 | from sqlalchemy.orm import ( 13 | Mapped, 14 | declarative_base, 15 | relationship, 16 | ) 17 | from sqlalchemy.testing.schema import Table 18 | 19 | Base = declarative_base() 20 | 21 | 22 | user_department_association_table = Table( 23 | "user_department", 24 | Base.metadata, 25 | Column("user_id", Integer, ForeignKey("user.id"), primary_key=True), 26 | Column("department_id", Integer, ForeignKey("department.id"), primary_key=True), 27 | ) 28 | 29 | 30 | class User(Base): 31 | __tablename__ = "user" 32 | 33 | id = Column(Integer, primary_key=True) 34 | name = Column(String) 35 | surname = Column(String) 36 | is_admin = Column(Boolean, default=False) 37 | city = Column(String) 38 | join_date = Column(DateTime(timezone=True), server_default=func.now()) 39 | departments: Mapped[List["Department"]] = relationship( 40 | secondary=user_department_association_table, back_populates="users" 41 | ) 42 | 43 | 44 | class Department(Base): 45 | __tablename__ = "department" 46 | 47 | id = Column(Integer, primary_key=True) 48 | name = Column(String) 49 | users: Mapped[List[User]] = relationship( 50 | secondary=user_department_association_table, 51 | back_populates="departments", 52 | ) 53 | -------------------------------------------------------------------------------- /src/pytest_sqlalchemy_mock/base.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | import pytest 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | from .model_mocker import ModelMocker 8 | 9 | if TYPE_CHECKING: 10 | from sqlalchemy.orm import Session 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | def connection_url(): 15 | return "sqlite:///:memory:" 16 | 17 | 18 | @pytest.fixture(scope="function") 19 | def engine(connection_url): 20 | return create_engine(connection_url) 21 | 22 | 23 | @pytest.fixture(scope="function") 24 | def sqlalchemy_declarative_base(): 25 | return 26 | 27 | 28 | @pytest.fixture(scope="function") 29 | def sqlalchemy_mock_config(): 30 | return 31 | 32 | 33 | @pytest.fixture(scope="function") 34 | def connection(engine, sqlalchemy_declarative_base): 35 | if sqlalchemy_declarative_base: 36 | sqlalchemy_declarative_base.metadata.create_all(engine) 37 | return engine.connect() 38 | 39 | 40 | @pytest.fixture(scope="function") 41 | def session(connection): 42 | session: Session = sessionmaker()(bind=connection) 43 | yield session 44 | session.close() 45 | 46 | 47 | @pytest.fixture(scope="function") 48 | def mocked_session(connection, sqlalchemy_declarative_base, sqlalchemy_mock_config): 49 | session: Session = sessionmaker()(bind=connection) 50 | 51 | if sqlalchemy_declarative_base and sqlalchemy_mock_config: 52 | ModelMocker(session, sqlalchemy_declarative_base, sqlalchemy_mock_config).create_all() 53 | 54 | yield session 55 | session.close() 56 | -------------------------------------------------------------------------------- /tests/test_pytest_sqlalchemy_mock.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | 3 | from .data import MockData 4 | from .db import ( 5 | Department, 6 | User, 7 | ) 8 | 9 | 10 | def test_get_session(session): 11 | assert session.execute(text("SELECT 5")).scalar() == 5 12 | 13 | 14 | def test_session_user_table(session): 15 | assert session.execute(text("SELECT count(*) from user")).scalar() == 0 16 | 17 | 18 | def test_session_query_for_assocation_table(session): 19 | assert session.execute(text("SELECT count(*) from user_department")).scalar() == 0 20 | 21 | 22 | def test_mocked_session_user_table(mocked_session): 23 | user_data = mocked_session.execute(text("SELECT * from user;")).first() 24 | raw_data = MockData.USER_DATA[0] 25 | assert user_data[0] == raw_data["id"] 26 | assert user_data[1] == raw_data["name"] 27 | assert user_data[2] == raw_data["surname"] 28 | assert user_data[3] == raw_data["is_admin"] 29 | assert user_data[4] == raw_data["city"] 30 | 31 | 32 | def test_mocked_session_user_model(mocked_session): 33 | user = mocked_session.query(User).filter_by(id=2).first() 34 | raw_data = MockData.USER_DATA[1] 35 | assert user.name == raw_data["name"] 36 | assert user.is_admin == raw_data["is_admin"] 37 | assert user.departments[0].name == "Sales" 38 | 39 | 40 | def test_mocked_session_department_model(mocked_session): 41 | department = mocked_session.query(Department).filter_by(id=1).first() 42 | raw_data = MockData.DEPARTMENT_DATA[0] 43 | assert department.name == raw_data["name"] 44 | assert len(department.users) == 1 45 | assert department.users[0].name == "Kevin" 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=69.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pytest-sqlalchemy-mock" 7 | version = "0.1.7" 8 | license.file = "LICENSE" 9 | description = "pytest sqlalchemy plugin for mock" 10 | authors = [ 11 | { name="Resul Yurttakalan", email="resulyrt93@gmail.com" }, 12 | ] 13 | requires-python = ">=3.9" 14 | readme = "README.md" 15 | classifiers = [ 16 | "Framework :: Pytest", 17 | "Development Status :: 3 - Alpha", 18 | "Topic :: Software Development :: Testing", 19 | "Intended Audience :: Developers", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "License :: OSI Approved :: MIT License", 26 | ] 27 | 28 | dependencies = [ 29 | "pytest>=7.0.0", 30 | "sqlalchemy>=2.0.6", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "black>=23.12.1", 36 | "build>=1.0.3", 37 | "flake8>=7.0.0", 38 | "isort>=5.13.2", 39 | "pre-commit>=3.6.0", 40 | "pytest-cov>=4.1.0", 41 | ] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/resulyrt93/pytest-sqlalchemy-mock" 45 | 46 | [tool.black] 47 | target-version = ["py312"] 48 | line-length = 120 49 | 50 | [tool.isort] 51 | force_grid_wrap = 2 52 | profile = "black" 53 | py_version = 312 54 | src_paths = ["src"] 55 | skip_glob = ["tests/conftest.py", "build/*", "dist/*",] 56 | 57 | [tool.pytest.ini_options] 58 | pythonpath = [ 59 | "src", 60 | ] 61 | 62 | [tool.pytest] 63 | norecursedirs = [ 64 | "dist", 65 | "build", 66 | ".git", 67 | ".tox", 68 | ".eggs", 69 | "venv", 70 | ] 71 | 72 | [project.entry-points."pytest11"] 73 | pytest_sqlalchemy_mock = "pytest_sqlalchemy_mock.base" 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | .idea 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-sqlalchemy-mock 2 | 3 | [![PyPI version](https://badge.fury.io/py/pytest-sqlalchemy-mock.svg)](https://badge.fury.io/py/pytest-sqlalchemy-mock) 4 | [![codecov](https://codecov.io/gh/resulyrt93/pytest-sqlalchemy-mock/branch/dev/graph/badge.svg?token=RUQ4DN3CH9)](https://codecov.io/gh/resulyrt93/pytest-sqlalchemy-mock) 5 | [![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) 6 | [![Supported Python Version](https://img.shields.io/pypi/pyversions/pytest-sqlalchemy-mock)](https://github.com/resulyrt93/pytest-sqlalchemy-mock) 7 | Code style: black 8 | 9 | This plugin provides pytest fixtures to create an in-memory DB instance on tests and dump your raw test data. 10 | 11 | ## Supported Python versions 12 | 13 | Python 3.12 or later highly recommended but also might work on Python 3.11. 14 | 15 | ## Installation 16 | 17 | ### Download from PyPI 18 | 19 | ```python 20 | pip install pytest-sqlalchemy-mock 21 | ``` 22 | 23 | ### Building from source 24 | 25 | At the top direcotry, 26 | 27 | ```sh 28 | python3 -m build 29 | python3 -m pip install dist/pytest_sqlalchemy_mock-*.whl 30 | ``` 31 | 32 | or 33 | 34 | ```sh 35 | python3 -m pip install . 36 | ``` 37 | 38 | ## Uninstall 39 | 40 | ```sh 41 | python3 -m pip uninstall pytest_sqlalchemy_mock 42 | ``` 43 | 44 | ## Usage 45 | 46 | Let's assume you have a SQLAlchemy declarative base and some models with it. 47 | 48 | ### models.py 49 | 50 | ```python 51 | from sqlalchemy import Column, Integer, String 52 | from sqlalchemy.orm import declarative_base 53 | 54 | Base = declarative_base() 55 | 56 | 57 | class User(Base): 58 | __tablename__ = "user" 59 | 60 | id = Column(Integer, primary_key=True) 61 | name = Column(String) 62 | ``` 63 | 64 | Firstly SQLAlchemy base class which is used for declare models should be passed with `sqlalchemy_declarative_base` fixture in `conftest.py` 65 | 66 | ### conftest.py 67 | 68 | ```python 69 | @pytest.fixture(scope="function") 70 | def sqlalchemy_declarative_base(): 71 | return Base 72 | ``` 73 | 74 | Then in test functions you can use `mocked_session` fixture to make query in mocked DB. 75 | 76 | ### test_user_table.py 77 | 78 | ```python 79 | def test_mocked_session_user_table(mocked_session): 80 | user_data = mocked_session.execute("SELECT * from user;").all() 81 | assert user_data == [] 82 | ``` 83 | 84 | Also, you can dump your mock data to DB before start testing via `sqlalchemy_mock_config` fixture like following. 85 | 86 | ### conftest.py 87 | 88 | ```python 89 | @pytest.fixture(scope="function") 90 | def sqlalchemy_mock_config(): 91 | return [("user", [ 92 | { 93 | "id": 1, 94 | "name": "Kevin" 95 | }, 96 | { 97 | "id": 2, 98 | "name": "Dwight" 99 | } 100 | ])] 101 | ``` 102 | 103 | ### test_user_table.py 104 | 105 | ```python 106 | def test_mocked_session_user_class(mocked_session): 107 | user = mocked_session.query(User).filter_by(id=2).first() 108 | assert user.name == "Dwight" 109 | ``` 110 | 111 | ## Upcoming Features 112 | 113 | * Mock with decorator for specific DB states for specific cases. 114 | * Support to load data from `.json` and `.csv` 115 | * Async SQLAlchemy support 116 | --------------------------------------------------------------------------------