Package: mock-alchemy
mock-alchemySimple and intuitive SQLAlchemy mock helpers.🤖 Mocking SQLAlchemySQLAlchemy is awesome.Unittests are great.Accessing DB during tests -
not so much. This library provides an easy way to mock SQLAlchemy's
session in unittests while preserving the ability to do sane asserts.Free software: MIT licenseGitHub:https://github.com/rajivsarvepalli/mock-alchemy📚 DocumentationFull documentation is available athttp://mock-alchemy.rtfd.io/. On
the documentation, you should be able to select a version of your choice
in order to view documentation of an older version if need be.This README includes some basic examples, but more detailed examples are
included in the documentation, especially in theuser guide. If you
are looking for an API reference, it is also available on thedocumentation.📦 InstallingYou can installmock-alchemyusing pip:$ pip install mock-alchemyIf you want to use this package on Python 2.7 or Python 3.6, then
installmock-alchemyusing:$ pip install "mock-alchemy>=0.1.0,<0.2.0"Pip should auto-detect the correct version but this ensures the correct
version is downloaded for your needs.🔖 VersioningThere are several different versions ofmock-alchemyavailable
depending on your needs. The versions0.1.xare available for use on
Python 2.7, Python 3.6+. The newer versions serve users who are on
Python 3.7+. For people interested in contributing, if you want to work
on Python 2.7 version checkout the branch0.1.xand then create pull-requests to that
branch. There is a set of specific tests run for that branch on pushes
and pull-requests since there are different tests for the newer versions
ofmock-alchemy. Check outcontributor guidefor more information.Documentation for the 0.1.0 version isavailable. However, the
current documentation should do a sufficient job at illustrating both
the past and the features of the present version at least as of now.
Therefore, I suggest using the most recent documentation for now, and if
you want, you can switch using the readthedocs version system (click on
the drop-down menu on the bottom right of the screen on the
documentation or go to theproject page).📤 CreditThe original library (alchemy-mock) was created by Miroslav
Shubernetskiy and Serkan Hoscai. This is a forked version due to a lack
of updates in the original library. It appeared that thealchemy-mockproject was no longer supported. Therefore, since I desired to add some
basic support for deleting, I created my own version of the library.Full credit goes to the original creators for starting and building this
project. You can find the original package onPyPiandGithub.⚙ UsingNormally SQLAlchemy's expressions cannot be easily compared
as comparison on binary expression produces yet another binary expression:>>> type((Model.foo == 5) == (Model.bar == 5))
<class 'sqlalchemy.sql.elements.BinaryExpression'>But they can be compared with this library:>>> ExpressionMatcher(Model.foo == 5) == (Model.bar == 5)
FalseExpressionMatchercan be directly used:>>> from mock_alchemy.comparison import ExpressionMatcher
>>> ExpressionMatcher(Model.foo == 5) == (Model.foo == 5)
TrueAlternativelyAlchemyMagicMockcan be used to mock out SQLAlchemy session:>>> from mock_alchemy.mocking import AlchemyMagicMock
>>> session = AlchemyMagicMock()
>>> session.query(Model).filter(Model.foo == 5).all()

>>> session.query.return_value.filter.assert_called_once_with(Model.foo == 5)In real world though session can be interacted with multiple times to query some data.
In those casesUnifiedAlchemyMagicMockcan be used which combines various calls for easier assertions:>>> from mock_alchemy.mocking import UnifiedAlchemyMagicMock
>>> session = UnifiedAlchemyMagicMock()

>>> m = session.query(Model)
>>> q = m.filter(Model.foo == 5)
>>> if condition:
...     q = q.filter(Model.bar > 10).all()
>>> data1 = q.all()
>>> data2 = m.filter(Model.note == 'hello world').all()

>>> session.filter.assert_has_calls([
...     mock.call(Model.foo == 5, Model.bar > 10),
...     mock.call(Model.note == 'hello world'),
... ])Also real-data can be stubbed by criteria:>>> from mock_alchemy.mocking import UnifiedAlchemyMagicMock
>>> session = UnifiedAlchemyMagicMock(data=[
...     (
...         [mock.call.query(Model),
...          mock.call.filter(Model.foo == 5, Model.bar > 10)],
...         [Model(foo=5, bar=11)]
...     ),
...     (
...         [mock.call.query(Model),
...          mock.call.filter(Model.note == 'hello world')],
...         [Model(note='hello world')]
...     ),
...     (
...         [mock.call.query(AnotherModel),
...          mock.call.filter(Model.foo == 5, Model.bar > 10)],
...         [AnotherModel(foo=5, bar=17)]
...     ),
... ])
>>> session.query(Model).filter(Model.foo == 5).filter(Model.bar > 10).all()
[Model(foo=5, bar=11)]
>>> session.query(Model).filter(Model.note == 'hello world').all()
[Model(note='hello world')]
>>> session.query(AnotherModel).filter(Model.foo == 5).filter(Model.bar > 10).all()
[AnotherModel(foo=5, bar=17)]
>>> session.query(AnotherModel).filter(Model.note == 'hello world').all()
[]TheUnifiedAlchemyMagicMockcan partially fake session mutations
such assession.add(instance). For example:>>> session = UnifiedAlchemyMagicMock()
>>> session.add(Model(pk=1, foo='bar'))
>>> session.add(Model(pk=2, foo='baz'))
>>> session.query(Model).all()
[Model(foo='bar'), Model(foo='baz')]
>>> session.query(Model).get(1)
Model(foo='bar')
>>> session.query(Model).get(2)
Model(foo='baz')Note that its partially correct since if added models are filtered on,
session is unable to actually apply any filters so it returns everything:>>> session.query(Model).filter(Model.foo == 'bar').all()
[Model(foo='bar'), Model(foo='baz')]Finally,UnifiedAlchemyMagicMockcan partially fake deleting. Anything that can be
accessed withallcan also be deleted. For example:>>> s = UnifiedAlchemyMagicMock()
>>> s.add(SomeClass(pk1=1, pk2=1))
>>> s.add_all([SomeClass(pk1=2, pk2=2)])
>>> s.query(SomeClass).all()
[1, 2]
>>> s.query(SomeClass).delete()
2
>>> s.query(SomeClass).all()
[]Note the limitation for dynamic sessions remains the same. Additionally, the delete will not be propagated across
queries (only unified in the exact same query). As in, if there are multiple queries in which the 'same'
object is present, this library considers them separate objects. For example:>>> s = UnifiedAlchemyMagicMock(data=[
...     (
...         [mock.call.query('foo'),
...          mock.call.filter(c == 'one', c == 'two')],
...         [SomeClass(pk1=1, pk2=1), SomeClass(pk1=2, pk2=2)]
...     ),
...     (
...         [mock.call.query('foo'),
...          mock.call.filter(c == 'one', c == 'two'),
...          mock.call.order_by(c)],
...         [SomeClass(pk1=2, pk2=2), SomeClass(pk1=1, pk2=1)]
...     ),
...     (
...         [mock.call.filter(c == 'three')],
...         [SomeClass(pk1=3, pk2=3)]
...     ),
...     (
...         [mock.call.query('foo'),
...          mock.call.filter(c == 'one', c == 'two', c == 'three')],
...         [SomeClass(pk1=1, pk2=1), SomeClass(pk1=2, pk2=2), SomeClass(pk1=3, pk2=3)]
...     ),
... ])

>>> s.query('foo').filter(c == 'three').delete()
1
>>> s.query('foo').filter(c == 'three').all()
[]
>>> s.query('foo').filter(c == 'one').filter(c == 'two').filter(c == 'three').all()
[1, 2, 3]The item referred to byc == 'three'is still present in the filtered query despite the individual item being deleted.👷 ContributingContributions are welcome. To learn more, see theContributor Guide.📕 LicenseDistributed under the terms of theMITlicense,mock-alchemyis
free and open source software.💥 IssuesIf you encounter any issues or problems, pleasefile an issuealong
with a detailed description.