Problems trying to mock a Model within Flask-SQLAlchemy

后端 未结 2 835
不知归路
不知归路 2021-02-04 16:54

I\'m testing a Flask application that have some SQLAlchemy models using Flask-SQLAlchemy and I\'m having some problems trying to mock a few models to some methods that receive s

2条回答
  •  轮回少年
    2021-02-04 17:20

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

    Let's look at a working code example and than 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 pretty clear and to the point. The fixture does all the heavy lifting:

    • MockedUser would replace the original User class - it would create a new mock object with the right spec every time it's needed
    • The purpose of MockedUserMeta has to be explained a bit further: SQLAlchemy has a nasty syntax which involves static functions. Imagine your tested code has a line similar to this from_db = User.query.filter(User.id == 20).one(), you should have a way to mock the response: MockedUserMeta.static_instance.query.filter.return_value.one.return_value.username = 'mocked_username'

    This is the best method that I found which allows to have tests without any db access and without any flask app, while allowing to mock SQLAlchemy query results.

    Since I don't like writing this boilerplate over and over, I have created a helper library to do it for me. Here is the code I wrote to generate the needed stuff for your example:

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

    The output is:

    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)
    

    Which is exactly what I needed to place in my fixture.

提交回复
热议问题