21

In brief

When testing a model class in Flask-SqlAlchemy, how can we mock the method .query.filter_by() so as to return the list of mocked model objects?

Full details

Let's say we have a model class as below code

from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class SomeModel(db.Model):
    # more column mapping and methods go here

Then in our Flask code we call

SomeModel.query.filter_by(...)

In our testing code, using Python unittest model with mocking, we want to mock the filter_by() call so that it returns a list of model objects under our designed test case.

How can we get to that?

p.s.

My google search only found this related post; though applying @patch("flask_sqlalchemy.SignallingSession", autospec=True) at the beginning of the class not work for me.

I also tried to mock the function as below code snippet

@patch('app.model.some_model.SomeModel.query.filter_by')
def test_some_case(self, filterbyMOCK):
    # more test logic goes here

and the code get immediate error when started

RuntimeError: application not registered on db instance and no application bound to current context

The full error from PyCharm IDE as snapshot below.

Traceback (most recent call last):
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1297, in patched
    arg = patching.__enter__()
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1353, in __enter__
    self.target = self.getter()
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1523, in <lambda>
    getter = lambda: _importer(target)
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1210, in _importer
    thing = _dot_lookup(thing, comp, import_path)
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1197, in _dot_lookup
    return getattr(thing, comp)
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py", line 428, in __get__
    return type.query_class(mapper, session=self.sa.session())
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 78, in __call__
    return self.registry()
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/sqlalchemy/util/_collections.py", line 990, in __call__
    return self.registry.setdefault(key, self.createfunc())
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py", line 136, in __init__
    self.app = db.get_app()
  File "/home/namgivu/NN/code/someproject-cloud/venv/local/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py", line 809, in get_app
    raise RuntimeError('application not registered on db '
RuntimeError: application not registered on db instance and no application bound to current context
Community
  • 1
  • 1
Nam G VU
  • 33,193
  • 69
  • 233
  • 372

2 Answers2

20

You'll have to mock the whole mapper class; accessing the query attribute on the mapper causes a session load:

@patch('app.model.some_model.SomeModel')
def test_some_case(self, some_model_mock):
    filter_by_mock = some_model_mock.query.filter_by
    # more test logic goes here

That's because the .query attribute is a descriptor object; accessing it triggers the binding to a session.

The alternative would be to mock out the _QueryProperty.__get__ method (which backs the .query attribute); only use this if you must test with actual SomeModel instances:

@patch('flask_sqlalchemy._QueryProperty.__get__')
def test_some_case(self, query_property_getter_mock):
    filter_by_mock = query_property_getter_mock.return_value.filter_by
    # more test logic goes here

Demo:

>>> from flask_sqlalchemy import SQLAlchemy
>>> db = SQLAlchemy()
>>> class SomeModel(db.Model):
...     id = db.Column(db.Integer, primary_key=True)
...
>>> from unittest import mock
>>> with mock.patch('__main__.SomeModel') as model_mock:
...     filter_by = model_mock.query.filter_by
...     SomeModel.query.filter_by(SomeModel.id == 'foo')
...
<MagicMock name='SomeModel.query.filter_by()' id='4438980312'>
>>> with mock.patch('flask_sqlalchemy._QueryProperty.__get__') as query_property_getter_mock:
...     filter_by_mock = query_property_getter_mock.return_value.filter_by
...     SomeModel.query.filter_by(SomeModel.id == 'foo')
...
<MagicMock name='__get__().filter_by()' id='4439035184'>
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    @NamGVU: the rest of your model will also depend on SQLAlchemy properties and is not part of the system under test. When you are testing if code using queries is working, you are not testing if the model is working (which is mostly SQLAlchemy's domain to test, not yours). It is better to just mock the whole model in that case. – Martijn Pieters Feb 27 '17 at 10:50
  • One more thing comes up today @Martijn Pieters, when do we go to inner attribute via `return_value` and when NOT to? – Nam G VU Mar 06 '17 at 05:35
  • 1
    @NamGVU: you can use a `()` call too, but you can't use that as an assignment target. So `filter_by_mock = query_property_getter_mock().filter_by` works, but `filter_by_mock() = faked_result_set` doesn't. So I personally prefer to use `.return_value` everywhere to keep code symmetrical and to aid readability to newcomers to my code and the Python mocking library. – Martijn Pieters Mar 06 '17 at 07:42
8

Just a sum-up from Martijn Pieters answer

Target

  • We want to mock .query.filter_by().all() result e.g. SomeModel.query.filter_by().all()

Code 01

@patch('flask_sqlalchemy._QueryProperty.__get__')
def test (
  self,
  queryMOCK,
):

  #setup
  queryMOCK\
    .return_value.filter_by\
    .return_value.all\
    .return_value = [1,22]

  #get actual
  modelObj = SomeModel.query.filter_by().all()
  print(modelObj)

Code 02 - similar as above and using with

def test(self):
  with patch('flask_sqlalchemy._QueryProperty.__get__') as queryMOCK      #setup
    queryMOCK\
      .return_value.filter_by\
      .return_value.all\
      .return_value = [1,22]

    #get actual
    modelObj = SomeModel.query.filter_by().all()
    print(modelObj)
Nam G VU
  • 33,193
  • 69
  • 233
  • 372