How to mock a imported object with pytest-mock or magicmock

穿精又带淫゛_ 提交于 2019-12-22 05:23:29

问题


I am trying to understand the mock/monkeypatch/pytest-mock capabilities.

Let me know if this is possible. If not could you please suggest how I can test this code.

My code structure:

/
./app
../__init__.py
../some_module1
.../__init__.py
../some_module2
.../__init__.py
./tests
../test_db.py

The /app/__init__.py is where my application (a Flask application if it helps) is started along with initializing a database connection object to a MongoDB database:

# ...

def create_app():
  # ...
  return app

db_conn = DB()

The some_module1 and some_module import the db_conn object and use it as part of their functions:

## some_module1/__init__.py
from app import db_conn

...
db = db_conn.db_name2.db_collection2

def some_func1():
    data = db.find()
    # check and do something with data
    return boolean_result

...

## some_module2/__init__.py
from app import db_conn

...
db = db_conn.db_name1.db_collection1

def some_func2():
    data = db.find()
    # check and do something with data
    return boolean_result
...

In my tests, I want to test if my code works properly based on the data received from the database. I want to mock the database, more specifically the db_conn object since I don't want to use a real database (which would be a lot of work setting up the environment and maintaining it).

Any suggestions on how I can emulate the db_conn?

I've been exploring pytest-mock and magicmock but I don't think or know how to mock the db_conn in my test.


回答1:


I believe you are right not testing cases on a real database because it's not unit testing anymore if you are using external dependencies.

There is a possibility to specify return-value and customize it (different return values on each iteration even) for Mock or MagicMock objects.

from unittest.mock import Mock, patch 

from app import db_conn


@patch('app.db_conn.find')
def test_some_func1(db_con_mock):
    ...
    assert ...

Keep in mind that in each patch you should specify the import path of db_conn - the path where db_conn **is used (I assume it's a different path in each test), not where it is defined.




回答2:


To answer the intial question "How to mock a imported object with pytest-mock or magicmock" you can do:

from unittest import mock  # because unittest's mock works great with pytest

def test_some_func1():
    with mock.patch('some_module1.db', mock.MagicMock(return_value=...)) as magicmock:
        result = some_func1(...)
        assert ... e.g. different fields of magicmock
        assert expected == result

# or alternatively use annotations

@mock.patch('some_module2.db', mock.MagicMock(return_value=...))
def test_some_func2():
        result = some_func2(...)

note that you do not patch the actual source of db

For your other use case

I want to mock the database (using a mongo database), more specifically the "db_conn" object

you similarly follow the hints of the link above:

mock.patch('some_module1.db_conn', mock.MagicMock(return_value=...))

Given that, you will notice in your tests that db from `db = db_conn.db_name2.db_collection2' will create another mock object. Calls to that object will be recorded as well. In such a way you will be able to trace history of calls and values assignments as well.


Furthermore, see an example how to pach mongo db.

For testing of Flask apps see the documentation of flask. Also this is a nice explanation as well, and uses DB connections.

As a general hint, like @MikeMajara mentioned - separate your code more into smaller functions that are also easy to test. In tradition to TDD: write tests first, implement later, and refactor (especially DRY!)




回答3:


Separation of concerns. Build methods that do one, and only one thing. Even more if you are going with TDD. In your example some_func2 does more than one. You could refactor as follows:

def get_object_from_db():
    return db.find()

def check_condition_on_object(obj):
    check something to do with object
    return true or false

def some_func2():
   obj = get_object_from_db()
   check_condition_on_object(obj)

With this approach you could easily test get_object_from_db and check_condition_on_object separately. This will improve readability, avoid bugs, and help detecting these if they appear at some point.


About "mocking an imported object". You might be trying to mock an object with a library that is meant for a more advance case than yours. These libraries provide you with a bunch of methods surrounding test environment out of the box that you might not need. From the looks, you just want to populate an object with mock data, and/or interact with a mocked db_connection instance. So...

To populate, I would simplify: You know the condition you want to test and you want to check if the result for a given object is the expected one. Just build yourself a test_object_provider.py that returns your known cases for true|false. No need to make things more complex.

To use a fake MongoDB connection you can try with mongomock. (although ideally you would test mongo with a real instance in a separate test).



来源:https://stackoverflow.com/questions/58377166/how-to-mock-a-imported-object-with-pytest-mock-or-magicmock

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!