试图在 Flask-SQLAlchemy 中模拟模型的问题
Problems trying to mock a Model within Flask-SQLAlchemy
我正在测试一个 Flask 应用程序,该应用程序具有一些使用 Flask-SQLAlchemy 的 SQLAlchemy 模型,我在尝试将一些模型模拟为一些接收一些模型作为参数的方法时遇到了一些问题。
我正在尝试做的玩具版本是这样的。假设我有一个模型:
// 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)
在使用应用工厂模式构建的应用中导入:
// 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)
以及一些需要 User
作为参数的函数:
// 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
此外,应该有一些视图和蓝图在 app.py
中导入并在应用程序中注册,后者在某处调用函数 get_user_age
。
我的问题是:我想测试功能 get_user_age
而无需创建应用程序、注册假数据库等。这不是必需的,该功能完全独立于它在 Flask 应用程序中使用的事实。
所以我尝试了:
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)
这引发了我的 RuntimeError: application not registered on db instance and no application bound to current context
异常。所以我想"yeah, obviously I must patch some object to prevent it from checking if the app is registered with the database and etc"。所以我尝试用 @mock.patch("database.SQLAlchemy")
和其他东西装饰它,但无济于事。
有谁知道我应该修补什么来防止这种行为,或者即使我的测试策略完全错误?
所以,我在键盘上敲了几个小时后找到了解决办法。问题好像是下面的(有知道的请指正)
当我 运行 mock.create_autospec(User)
时,mock 模块会尝试检查 User
的所有属性,以便为它将吐出的 Mock 对象创建足够的规范。发生这种情况时,它会尝试检查属性 User.query
,只有当您在 Flask 应用程序的范围内时才能对其进行评估。
发生这种情况是因为在评估 User.query
时,会创建一个需要有效会话的对象。此会话由 Flask-SQLAlchemy 中 SQLAlchemy
class 上的 create_scope_session
方法创建。
此方法实例化一个名为 SignallingSession
的 class,其 __init__
方法调用 SQLAlchemy.get_app
方法。这是在数据库中没有注册应用程序时引发 RuntimeError
的方法。
通过修补 SignallingSession 方法,一切正常。因为我不想与数据库交互,所以没关系:
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)
我找到了解决这个问题的另一种方法。基本思想是控制对静态属性的访问。我使用了 pytest 和 mocker,但代码可以修改为使用 unittest。
让我们看一个工作代码示例并对其进行解释:
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))
测试非常清楚,切中要害。该夹具完成了所有繁重的工作:
MockedUser
将替换原来的 User
class - 它会在每次需要时创建一个具有正确规范的新模拟对象
MockedUserMeta
的目的需要进一步解释:SQLAlchemy 有一个涉及静态函数的令人讨厌的语法。想象一下你的测试代码有一行类似于 from_db = User.query.filter(User.id == 20).one()
,你应该有办法模拟响应:MockedUserMeta.static_instance.query.filter.return_value.one.return_value.username = 'mocked_username'
这是我发现的最好的方法,它允许在没有任何数据库访问和任何烧瓶应用程序的情况下进行测试,同时允许模拟 SQLAlchemy 查询结果。
因为我不喜欢一遍又一遍地写这个样板文件,所以 I have created a helper library 帮我写。这是我为您的示例生成所需内容而编写的代码:
from mock_autogen.pytest_mocker import PytestMocker
print(PytestMocker(database).mock_classes().mock_classes_static().generate())
输出为:
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)
这正是我需要放置在灯具中的东西。
我正在测试一个 Flask 应用程序,该应用程序具有一些使用 Flask-SQLAlchemy 的 SQLAlchemy 模型,我在尝试将一些模型模拟为一些接收一些模型作为参数的方法时遇到了一些问题。
我正在尝试做的玩具版本是这样的。假设我有一个模型:
// 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)
在使用应用工厂模式构建的应用中导入:
// 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)
以及一些需要 User
作为参数的函数:
// 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
此外,应该有一些视图和蓝图在 app.py
中导入并在应用程序中注册,后者在某处调用函数 get_user_age
。
我的问题是:我想测试功能 get_user_age
而无需创建应用程序、注册假数据库等。这不是必需的,该功能完全独立于它在 Flask 应用程序中使用的事实。
所以我尝试了:
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)
这引发了我的 RuntimeError: application not registered on db instance and no application bound to current context
异常。所以我想"yeah, obviously I must patch some object to prevent it from checking if the app is registered with the database and etc"。所以我尝试用 @mock.patch("database.SQLAlchemy")
和其他东西装饰它,但无济于事。
有谁知道我应该修补什么来防止这种行为,或者即使我的测试策略完全错误?
所以,我在键盘上敲了几个小时后找到了解决办法。问题好像是下面的(有知道的请指正)
当我 运行 mock.create_autospec(User)
时,mock 模块会尝试检查 User
的所有属性,以便为它将吐出的 Mock 对象创建足够的规范。发生这种情况时,它会尝试检查属性 User.query
,只有当您在 Flask 应用程序的范围内时才能对其进行评估。
发生这种情况是因为在评估 User.query
时,会创建一个需要有效会话的对象。此会话由 Flask-SQLAlchemy 中 SQLAlchemy
class 上的 create_scope_session
方法创建。
此方法实例化一个名为 SignallingSession
的 class,其 __init__
方法调用 SQLAlchemy.get_app
方法。这是在数据库中没有注册应用程序时引发 RuntimeError
的方法。
通过修补 SignallingSession 方法,一切正常。因为我不想与数据库交互,所以没关系:
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)
我找到了解决这个问题的另一种方法。基本思想是控制对静态属性的访问。我使用了 pytest 和 mocker,但代码可以修改为使用 unittest。
让我们看一个工作代码示例并对其进行解释:
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))
测试非常清楚,切中要害。该夹具完成了所有繁重的工作:
MockedUser
将替换原来的User
class - 它会在每次需要时创建一个具有正确规范的新模拟对象MockedUserMeta
的目的需要进一步解释:SQLAlchemy 有一个涉及静态函数的令人讨厌的语法。想象一下你的测试代码有一行类似于from_db = User.query.filter(User.id == 20).one()
,你应该有办法模拟响应:MockedUserMeta.static_instance.query.filter.return_value.one.return_value.username = 'mocked_username'
这是我发现的最好的方法,它允许在没有任何数据库访问和任何烧瓶应用程序的情况下进行测试,同时允许模拟 SQLAlchemy 查询结果。
因为我不喜欢一遍又一遍地写这个样板文件,所以 I have created a helper library 帮我写。这是我为您的示例生成所需内容而编写的代码:
from mock_autogen.pytest_mocker import PytestMocker
print(PytestMocker(database).mock_classes().mock_classes_static().generate())
输出为:
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)
这正是我需要放置在灯具中的东西。