Alchemy-Mock
Alchemy-Mock (version 0.4.3) provides helpers for mocking SQLAlchemy sessions in unit tests, allowing for assertions against SQLAlchemy expressions. The project appears to be abandoned since its last release in 2019, with a community-maintained fork, `mock-alchemy`, now serving as an actively developed alternative.
Common errors
-
AttributeError: 'MagicMock' object has no attribute 'filter' (or '.all', '.first', etc.)
cause This typically occurs when a mock in a chained call is not configured with `return_value`, so an intermediate call returns a generic `MagicMock` instead of a mock configured to respond to the next method in the chain.fixEnsure all parts of your SQLAlchemy query chain are correctly mocked with `return_value`. For example, instead of `session.query.filter.return_value = ...`, use `session.query.return_value.filter.return_value = ...` or use `UnifiedAlchemyMagicMock` which handles common chains. -
TypeError: 'BinaryExpression' object is not callable
cause Attempting to pass a SQLAlchemy binary expression (e.g., `User.id == 1`) directly to `mock.call(...)` or `assert_called_with` when the underlying mock expects a comparable object, but the mock library doesn't know how to compare the SQLAlchemy expression type.fixWrap SQLAlchemy expressions with `ExpressionMatcher` from `alchemy_mock.comparison` when asserting calls that involve such expressions. For example, `mock_session.filter.assert_called_once_with(ExpressionMatcher(User.name == 'Alice'))`. -
TypeError: object of type 'MagicMock' has no len() (or 'is not subscriptable') when mocking scalar()
cause This indicates an incorrect return type for a `scalar()` mock. When querying a column with `scalar()`, the mock expects an indexable sequence (e.g., `[value]`). When querying a table and getting a single object, it expects a non-indexable single object.fixAdjust the mocked data for `scalar()`: if it's a column query, provide data as `[value]`. If it's a table query returning a single object, provide data as `[obj]`, ensuring the mock returns `obj` directly for `scalar()`.
Warnings
- breaking The `alchemy-mock` project appears to be abandoned, with no new releases since November 2019. For ongoing development and better Python version compatibility, consider migrating to the actively maintained fork `mock-alchemy` (PyPI: `mock-alchemy`, GitHub: `rajivsarvepalli/mock-alchemy`).
- gotcha When using `UnifiedAlchemyMagicMock`, `session.query(Model).filter(...)` will return all data provided in `UnifiedAlchemyMagicMock(data=...)` if you've also added models to the session, as the mock does not perform actual filtering on the added models.
- gotcha Mocking `scalar()` has specific behaviors: querying on columns expects an indexable sequence from the mocked data, while querying on a table expects a non-indexable object. Misconfiguration can lead to unexpected `TypeError` or incorrect return values.
- deprecated The original `alchemy-mock` repository has an open issue (#35) regarding 'Importing ABC directly from collections will be removed in Python 3.10', indicating potential compatibility issues with Python 3.10+.
Install
-
pip install alchemy-mock
Imports
- AlchemyMagicMock
from alchemy_mock.mocking import AlchemyMagicMock
- UnifiedAlchemyMagicMock
from alchemy_mock.mocking import UnifiedAlchemyMagicMock
- ExpressionMatcher
from alchemy_mock.comparison import ExpressionMatcher
Quickstart
import unittest
from unittest import mock
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from alchemy_mock.mocking import UnifiedAlchemyMagicMock
# Define a simple SQLAlchemy model for demonstration
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
def __repr__(self):
return f"<User(id={self.id}, name='{self.name}', email='{self.email}')>"
# Example usage of UnifiedAlchemyMagicMock
def get_user_by_name(session, name):
return session.query(User).filter(User.name == name).first()
def add_user(session, name, email):
user = User(name=name, email=email)
session.add(user)
session.commit()
return user
class TestUserService(unittest.TestCase):
def test_get_user_by_name(self):
mock_session = UnifiedAlchemyMagicMock(
data=[([mock.call.query(User), mock.call.filter(User.name == 'Alice')], [User(id=1, name='Alice', email='alice@example.com')])]
)
user = get_user_by_name(mock_session, 'Alice')
self.assertIsNotNone(user)
self.assertEqual(user.name, 'Alice')
mock_session.filter.assert_called_once_with(User.name == 'Alice')
def test_add_user(self):
mock_session = UnifiedAlchemyMagicMock()
new_user = add_user(mock_session, 'Bob', 'bob@example.com')
# In alchemy-mock, the mock session doesn't apply filters to added models, so querying them directly might not work as expected.
# However, you can assert that the add and commit calls happened.
mock_session.add.assert_called_once()
mock_session.commit.assert_called_once()
# To retrieve added data, you typically would configure 'data' if querying after adding.
# Or, assert on the object passed to add.
self.assertEqual(new_user.name, 'Bob')
self.assertEqual(new_user.email, 'bob@example.com')
# To run the tests (example):
if __name__ == '__main__':
unittest.main()