试图在Flask-SQLAlchemy中模拟模型的问题

Raf*_*ini 7 testing mocking flask flask-sqlalchemy

我正在使用Flask-SQLAlchemy测试一个带有一些SQLAlchemy模型的Flask应用程序,并且我在尝试将一些模型模拟为某些接收某些模型作为参数的方法时遇到了一些问题.

我正在尝试做的玩具版本是这样的.假设我有一个模型给出:

// 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)
Run Code Online (Sandbox Code Playgroud)

这是在使用应用工厂模式构建的应用中导入的:

// 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)
Run Code Online (Sandbox Code Playgroud)

还有一些需要Useras参数的函数:

// 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
Run Code Online (Sandbox Code Playgroud)

此外,应用程序中应该导入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)
Run Code Online (Sandbox Code Playgroud)

这引起了我的RuntimeError: application not registered on db instance and no application bound to current context例外.所以我想"是的,显然我必须修补一些对象,以防止它检查应用程序是否已注册数据库等".所以我尝试用@mock.patch("database.SQLAlchemy")其他东西装饰它无济于事.

有谁知道我应该修补什么来防止这种行为,或者即使我的测试策略都错了?

Raf*_*ini 9

所以,我在键盘上敲了几个小时后发现了一个解决方案.问题似乎如下(如果有人知道更好,请纠正我).

当我运行时mock.create_autospec(User),mock模块会尝试检查所有属性,User以便为它将吐出的Mock对象创建适当的规范.发生这种情况时,它会尝试检查属性User.query,只有在Flask应用程序范围内时才能对其进行评估.

发生这种情况是因为在User.query评估时,会创建一个需要有效会话的对象.此会话由Flask-SQLAlchemy中create_scope_sessionSQLAlchemy类上的方法创建.

此方法实例化一个名为SignallingSession__init__方法调用该SQLAlchemy.get_app方法的类.这是RuntimeError在db中没有注册应用程序时引发的方法.

通过修补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)
Run Code Online (Sandbox Code Playgroud)


Pet*_*r K 6

我找到了解决这个问题的另一种方法。基本思想是控制对静态属性的访问。我使用了 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))
Run Code Online (Sandbox Code Playgroud)

测试非常清楚并且切题。该夹具完成了所有繁重的工作:

  • MockedUser将替换原始User类 - 每次需要时它都会创建一个具有正确规范的新模拟对象
  • MockedUserMeta必须进一步解释一下的目的:SQLAlchemy 有一个涉及静态函数的令人讨厌的语法。想象一下您的测试代码有一行类似于此的行from_db = User.query.filter(User.id == 20).one(),您应该有一种方法来模拟响应:MockedUserMeta.static_instance.query.filter.return_value.one.return_value.username = 'mocked_username'

这是我发现的最好的方法,它允许在没有任何数据库访问和任何 Flask 应用程序的情况下进行测试,同时允许模拟 SQLAlchemy 查询结果。

由于我不喜欢一遍又一遍地编写这个样板文件,所以我创建了一个帮助程序库来为我做这件事。这是我编写的用于生成示例所需内容的代码:

from mock_autogen.pytest_mocker import PytestMocker
print(PytestMocker(database).mock_classes().mock_classes_static().generate())
Run Code Online (Sandbox Code Playgroud)

输出是:

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)
Run Code Online (Sandbox Code Playgroud)

这正是我需要放置在我的固定装置中的东西。