Как смоделировать импортированный объект с помощью pytest-mock или magicmock

Я пытаюсь понять mock/monkeypatch/pytest-mock возможности.

Сообщите мне, возможно ли это. Если нет, не могли бы вы подсказать, как я могу протестировать этот код.

Моя структура кода:

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

В /app/__init__.py Здесь мое приложение (приложение Flask, если оно помогает) запускается вместе с инициализацией объекта подключения к базе данных в базе данных MongoDB:

# ...

def create_app():
  # ...
  return app

db_conn = DB()

В some_module1 а также some_module импортировать db_conn объект и использовать его как часть своих функций:

## 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
...

В своих тестах я хочу проверить, правильно ли работает мой код на основе данных, полученных из базы данных. Я хочу издеваться над базой данных, в частностиdb_conn объект, поскольку я не хочу использовать настоящую базу данных (что потребовало бы много работы по настройке среды и ее обслуживанию).

Любые предложения о том, как я могу подражать db_conn?

Я исследовал pytest-mock а также magicmock но я не думаю и не знаю, как издеваться над db_conn в моем тесте.

3 ответа

Я считаю, что вы правы, не тестируя кейсы на реальной базе данных, потому что это больше не модульное тестирование, если вы используете внешние зависимости.

Есть возможность указатьreturn-value и настроить его (даже разные возвращаемые значения на каждой итерации) дляMock или MagicMock объекты.

from unittest.mock import Mock, patch 

from app import db_conn


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

Имейте в виду, что в каждом patch вы должны указать путь импорта db_conn- путь, по которомуdb_conn** используется (я предполагаю, что это другой путь в каждом тесте), а не там, где он определен.

Чтобы ответить на основной вопрос "Как имитировать импортированный объект с помощью pytest-mock или magicmock", вы можете сделать:

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(...)

обратите внимание, что вы не исправляете фактический источник db

Для вашего другого варианта использования

Я хочу имитировать базу данных (используя базу данных mongo), а точнее объект "db_conn"

вы аналогичным образом следуете подсказкам по ссылке выше:

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

Учитывая это, вы заметите в своих тестах, что dbfrom `db = db_conn.db_name2.db_collection2'создаст еще один фиктивный объект. Звонки на этот объект также будут записаны. Таким образом, вы сможете отслеживать историю звонков и присвоение значений.


Кроме того, посмотрите пример подключения mongo db.

Для тестирования приложений Flask см. Документацию по flask. Также это хорошее объяснение и использует соединения с БД.

В качестве общего совета, например, упомянутого @MikeMajara, разделите свой код на более мелкие функции, которые также легко проверить. По традиции TDD: сначала пишите тесты, потом реализуйте и рефакторинг (особенно DRY!)

Разделение проблем. Создавайте методы, которые делают одно и только одно. Даже больше, если вы собираетесь использовать TDD. В вашем примере some_func2 делает более одного. Вы можете провести рефакторинг следующим образом:

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)

При таком подходе вы можете легко протестировать get_object_from_db а также check_condition_on_objectраздельно. Это улучшит читаемость, позволит избежать ошибок и поможет обнаружить их, если они появятся в какой-то момент.


Насчет "издевательства над импортированным объектом". Возможно, вы пытаетесь имитировать объект с помощью библиотеки, которая предназначена для более сложных случаев, чем ваша. Эти библиотеки предоставляют вам набор методов, окружающих тестовую среду, из коробки, которые могут вам не понадобиться. Судя по внешнему виду, вы просто хотите заполнить объект фиктивными данными и / или взаимодействовать с фиктивным экземпляром db_connection. Так...

Чтобы заполнить, я бы упростил: вы знаете условие, которое хотите проверить, и хотите проверить, является ли результат для данного объекта ожидаемым. Просто создай себеtest_object_provider.py который возвращает ваши известные случаи для true|false. Не нужно усложнять вещи.

Чтобы использовать поддельное соединение MongoDB, вы можете попробовать mongomock. (хотя в идеале вы бы протестировали mongo с реальным экземпляром в отдельном тесте).

Другие вопросы по тегам