Problems trying to mock a model in Flask-SQLAlchemy

I am testing a Flask application that has some SQLAlchemy models using Flask-SQLAlchemy and I'm having some problems trying to mock some models with some methods that get some models as parameters.

The toy version of what I'm trying to do is this. Suppose I have a model defined by:

// file: database.py from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True) birthday = db.Column(db.Date) 

This is imported into the application that is built using the factory application template:

 // file: app.py from flask import Flask from database import db def create_app(): app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' db.init_app(app) 

And some function that needs a User parameter like:

 // file: actions.py import datetime SECONDS_IN_A_YEAR = 31556926 def get_user_age(user): return (datetime.date.today() - user.birthday).total_seconds() // SECONDS_IN_A_YEAR 

In addition, there should be several views and drawings that are imported into app.py and registered in the application, which the latter calls the get_user_age function somewhere.

My problem: I want to test the get_user_age function without having to create an application, register in a fake database, etc. etc. This is not necessary, the function is completely independent of the fact that it is used in the Flask application.

So I tried:

 import unittest import datetime import mock from database import User from actions import get_user_age class TestModels(unittest.TestCase): def test_get_user_age(self): user = mock.create_autospec(User, instance=True) user.birthday = datetime.date(year=1987, month=12, day=1) print get_user_age(user) 

This throws me a RuntimeError: application not registered on db instance and no application bound to current context exception RuntimeError: application not registered on db instance and no application bound to current context . So I thought: β€œYes, I have to pay some kind of object so that it cannot check if the application is registered in the database, etc.” So I tried to decorate it with @mock.patch("database.SQLAlchemy") and other things to no avail.

Does anyone know what I should correct to prevent this behavior, or even if my test strategy is wrong?

+7
source share
2 answers

I found another way around this problem. The basic idea is to control access to static attributes. I used pytest and mocker, but the code could be adapted to use unittest.

Let's look at an example of working code and how to explain it:

 import pytest import datetime import database from actions import get_user_age @pytest.fixture def mock_user_class(mocker): class MockedUserMeta(type): static_instance = mocker.MagicMock(spec=database.User) def __getattr__(cls, key): return MockedUserMeta.static_instance.__getattr__(key) class MockedUser(metaclass=MockedUserMeta): original_cls = database.User instances = [] def __new__(cls, *args, **kwargs): MockedUser.instances.append( mocker.MagicMock(spec=MockedUser.original_cls)) MockedUser.instances[-1].__class__ = MockedUser return MockedUser.instances[-1] mocker.patch('database.User', new=MockedUser) class TestModels: def test_test_get_user_age(self, mock_user_class): user = database.User() user.birthday = datetime.date(year=1987, month=12, day=1) print(get_user_age(user)) 

The test is quite clear and to the point. The fixture does all the hard work:

  • MockedUser will replace the original User class - it will create a new dummy object with the desired specification every time it needs to
  • The purpose of MockedUserMeta should be explained a bit further: SQLAlchemy has an unpleasant syntax that includes static functions. Imagine that your tested code has a line similar to this from_db = User.query.filter(User.id == 20).one() , you should have a way to model the answer: MockedUserMeta.static_instance.query.filter.return_value.one.return_value.username = 'mocked_username'

This is the best method I have found that allows you to run tests without access to the database and without using the bulb application, and allows you to simulate the results of a SQLAlchemy query.

Since I do not like to write this template over and over, I created a helper library to do this for me. Here is the code I wrote to create the necessary material for your example:

 from mock_autogen.pytest_mocker import PytestMocker print(PytestMocker(database).mock_classes().mock_classes_static().generate()) 

Conclusion:

 class MockedUserMeta(type): static_instance = mocker.MagicMock(spec=database.User) def __getattr__(cls, key): return MockedUserMeta.static_instance.__getattr__(key) class MockedUser(metaclass=MockedUserMeta): original_cls = database.User instances = [] def __new__(cls, *args, **kwargs): MockedUser.instances.append(mocker.MagicMock(spec=MockedUser.original_cls)) MockedUser.instances[-1].__class__ = MockedUser return MockedUser.instances[-1] mocker.patch('database.User', new=MockedUser) 

This is exactly what I needed to put in my device.

+1
source

So, I found a solution after I tapped my head on the keyboard for a few seconds. The problem is this (if someone knows better, please correct me).

When I run mock.create_autospec(User) , the mock module tries to check all User attributes to create an adequate specification for the Mock object that it spits out. When this happens, it tries to check the User.query attribute, which can only be evaluated when you are in the Flask application area.

This is because evaluating User.query creates an object that requires a valid session. This session is created by the create_scope_session method in the SQLAlchemy class inside Flask-SQLAlchemy.

This method creates an instance of the SignallingSession class, whose __init__ method calls the SQLAlchemy.get_app method. This is a method that raises a RuntimeError when there is no application in db.

Replacing the SignallingSession method, everything works well. Since I do not want to interact with the database, this is normal:

 import unittest import datetime import mock from actions import age @mock.patch("flask_sqlalchemy.SignallingSession", autospec=True) class TestModels(unittest.TestCase): def test_age(self, session): import database user = mock.create_autospec(database.User) user.birthday = datetime.date(year=1987, month=12, day=1) print age(user) 
+9
source

Source: https://habr.com/ru/post/1212103/


All Articles