Как издеваться над импортом
Модуль A
включает в себя import B
на его вершине. Однако в тестовых условиях я хотел бы издеваться B
в A
(фиктивный A.B
) и полностью воздерживаться от импорта B
,
По факту, B
не установлен в тестовой среде специально.
А - это проверяемая единица. Я должен импортировать А со всеми его функциями. B - это модуль, который мне нужен для макета. Но как я могу высмеять B внутри A и остановить A от импорта реального B, если первое, что делает A, это импорт B?
(Причина, по которой B не установлен, заключается в том, что я использую pypy для быстрого тестирования, и, к сожалению, B пока не совместим с pypy.)
Как это можно сделать?
10 ответов
Вы можете назначить sys.modules['B']
перед импортом A
чтобы получить то, что вы хотите:
test.py:
import sys
sys.modules['B'] = __import__('mock_B')
import A
print(A.B.__name__)
A.py:
import B
Примечание B.py не существует, но при запуске test.py
ошибка не возвращается и print(A.B.__name__)
печать mock_B
, Вы все еще должны создать mock_B.py
где вы издеваетесь над фактическими функциями B / переменными / и т.д. Или вы можете просто назначить Mock() напрямую:
test.py:
import sys
sys.modules['B'] = Mock()
import A
Встроенный __import__
может быть изменен с помощью библиотеки 'mock' для большего контроля:
# Store original __import__
orig_import = __import__
# This will be the B module
b_mock = mock.Mock()
def import_mock(name, *args):
if name == 'B':
return b_mock
return orig_import(name, *args)
with mock.patch('__builtin__.__import__', side_effect=import_mock):
import A
Сказать A
похоже:
import B
def a():
return B.func()
A.a()
возвращается b_mock.func()
который также может быть высмеян.
b_mock.func.return_value = 'spam'
A.a() # returns 'spam'
Как издеваться над импортом, (издеваться над AB)?
Модуль A включает импорт B в его верхней части.
Легко, просто смоделируйте библиотеку в sys.modules, прежде чем она будет импортирована:
if wrong_platform():
sys.modules['B'] = mock.MagicMock()
а потом, пока A
не полагается на конкретные типы данных, возвращаемых из объектов B:
import A
должен просто работать.
Вы также можете издеваться import A.B
:
Это работает, даже если у вас есть подмодули, но вы захотите смоделировать каждый модуль. Скажем, у вас есть это:
from foo import This, That, andTheOtherThing
from foo.bar import Yada, YadaYada
from foo.baz import Blah, getBlah, boink
Чтобы смоделировать, просто сделайте ниже, прежде чем импортировать модуль, который содержит выше:
sys.modules['foo'] = MagicMock()
sys.modules['foo.bar'] = MagicMock()
sys.modules['foo.baz'] = MagicMock()
(Мой опыт: у меня была зависимость, которая работает на одной платформе, Windows, но не работала на Linux, где мы проводим наши ежедневные тесты. Поэтому мне нужно было смоделировать зависимость для наших тестов. К счастью, это был черный ящик, поэтому Мне не нужно было устанавливать много взаимодействия.)
Дразнящие побочные эффекты
Приложение: На самом деле мне нужно было смоделировать побочный эффект, который занял некоторое время. Поэтому мне нужен был метод объекта, чтобы поспать на секунду. Это будет работать так:
sys.modules['foo'] = MagicMock()
sys.modules['foo.bar'] = MagicMock()
sys.modules['foo.baz'] = MagicMock()
# setup the side-effect:
from time import sleep
def sleep_one(*args):
sleep(1)
# this gives us the mock objects that will be used
from foo.bar import MyObject
my_instance = MyObject()
# mock the method!
my_instance.method_that_takes_time = mock.MagicMock(side_effect=sleep_one)
И затем выполнение кода занимает некоторое время, как настоящий метод.
Ответ Аарона Холла работает для меня. Просто хочу упомянуть одну важную вещь,
если в A.py
ты сделаешь
from B.C.D import E
затем в test.py
вы должны издеваться над каждым модулем на пути, иначе вы получите ImportError
sys.modules['B'] = mock.MagicMock()
sys.modules['B.C'] = mock.MagicMock()
sys.modules['B.C.D'] = mock.MagicMock()
Я понимаю, что немного опоздал на вечеринку здесь, но вот несколько безумный способ автоматизировать это с mock
библиотека:
(вот пример использования)
import contextlib
import collections
import mock
import sys
def fake_module(**args):
return (collections.namedtuple('module', args.keys())(**args))
def get_patch_dict(dotted_module_path, module):
patch_dict = {}
module_splits = dotted_module_path.split('.')
# Add our module to the patch dict
patch_dict[dotted_module_path] = module
# We add the rest of the fake modules in backwards
while module_splits:
# This adds the next level up into the patch dict which is a fake
# module that points at the next level down
patch_dict['.'.join(module_splits[:-1])] = fake_module(
**{module_splits[-1]: patch_dict['.'.join(module_splits)]}
)
module_splits = module_splits[:-1]
return patch_dict
with mock.patch.dict(
sys.modules,
get_patch_dict('herp.derp', fake_module(foo='bar'))
):
import herp.derp
# prints bar
print herp.derp.foo
Причина, по которой это так нелепо сложно, заключается в том, что когда импорт происходит, Python в основном делает это (например, from herp.derp import foo
)
- Есть ли
sys.modules['herp']
существовать? Остальное импортируй. Если все еще нетImportError
- Есть ли
sys.modules['herp.derp']
существовать? Остальное импортируй. Если все еще нетImportError
- Получить атрибут
foo
изsys.modules['herp.derp']
, ещеImportError
foo = sys.modules['herp.derp'].foo
У этого взломанного совместного решения есть некоторые недостатки: если что-то еще зависит от других элементов в пути модуля, этот тип переворачивает его. Кроме того, это работает только для вещей, которые импортируются встроенными, таких как
def foo():
import herp.derp
или же
def foo():
__import__('herp.derp')
Я нашел хороший способ издеваться над импортом в Python. Здесь находится решение Эрика Zaadi, которое я использую в своем приложении Django.
У меня есть класс SeatInterface
который является интерфейсом к Seat
Модельный класс. Так что внутри моего seat_interface
модуль у меня такой импортный:
from ..models import Seat
class SeatInterface(object):
(...)
Я хотел создать изолированные тесты для SeatInterface
класс с издевались Seat
класс как FakeSeat
, Проблема была в том, как запускать тесты в автономном режиме, когда приложение Django не работает. У меня была ниже ошибка:
Неправильно сконфигурировано: запрашивается настройка BASE_DIR, но настройки не настроены. Вы должны либо определить переменную окружения DJANGO_SETTINGS_MODULE, либо вызвать settings.configure(), прежде чем получить доступ к настройкам.
Пробежал 1 тест за 0.078с
СБОЙ (ошибки =1)
Решение было:
import unittest
from mock import MagicMock, patch
class FakeSeat(object):
pass
class TestSeatInterface(unittest.TestCase):
def setUp(self):
models_mock = MagicMock()
models_mock.Seat.return_value = FakeSeat
modules = {'app.app.models': models_mock}
patch.dict('sys.modules', modules).start()
def test1(self):
from app.app.models_interface.seat_interface import SeatInterface
И тогда тест волшебным образом проходит нормально:)
,
Пробежал 1 тест за 0,002 сХорошо
Если вы делаете import ModuleB
вы действительно вызываете встроенный метод __import__
как:
ModuleB = __import__('ModuleB', globals(), locals(), [], -1)
Вы можете перезаписать этот метод, импортировав __builtin__
модуль и сделать обертку вокруг __builtin__.__import__
метод. Или вы могли бы поиграть с NullImporter
крюк от imp
модуль. Перехват исключения и макет вашего модуля / класса в except
-блок.
Указатель на соответствующие документы:
docs.python.org:__import__
Доступ к внутренним объектам импорта с помощью модуля Imp
Надеюсь, это поможет. НАСТОЯТЕЛЬНО предупреждаем, что вы вступаете в более загадочные периметры программирования на Python и что а) глубокое понимание того, чего вы действительно хотите достичь, и б) глубокое понимание последствий очень важно.
Я знаю, что это довольно старый вопрос, но в последнее время я возвращался к нему несколько раз и хотел поделиться кратким решением этого вопроса.
import sys
from unittest import mock
def mock_module_import(module):
def _outer_wrapper(func):
def _inner_wrapper(*args, **kwargs):
orig = sys.modules.get(module) # get the original module, if present
sys.modules[module] = mock.MagicMock() # patch it
try:
return func(*args, **kwargs)
finally:
if orig is not None: # if the module was installed, restore patch
sys.modules[module] = orig
else: # if the module never existed, remove the key
del sys.modules[module]
return _inner_wrapper
return _outer_wrapper
Он работает, временно исправляя ключ для модуля в sys.modules
, а затем восстановление исходного модуля после вызова декорированной функции. Это можно использовать в сценариях, где пакет может не быть установлен в тестовой среде, или в более сложном сценарии, когда исправленный модуль может фактически выполнять некоторые из своих собственных внутренних исправлений обезьяны (что было тем случаем, с которым я столкнулся).
Вот пример использования:
@mock_module_import("some_module")
def test_foo():
assert True
Сегодня я столкнулся с похожей проблемой, и я решил решить ее немного по-другому. Вместо того, чтобы взламывать механизм импорта Python, вы можете просто добавить фиктивный модуль в , и Python предпочтет его исходному модулю.
Создайте замещающий модуль в подкаталоге, например:
mkdir -p test/mocked-lib ${EDITOR} test/mocked-lib/B.py
До
A
импортируется, вставьте этот каталог в . Я использую pytest , поэтому в моемtest/conftest.py
, я просто сделал:import os.path import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "mocked-lib"))
Теперь, когда запускается набор тестов, подкаталог добавляется вsys.path
иimport A
используетB
отmocked-lib
.
Вот один из них, использующийcontextmanager
чтобы тесты выглядели чище:
Function being tested
def read_secret_from_databricks(scope: str, key: str) -> str:
from pyspark.dbutils import DBUtils
return DBUtils(spark).secrets.get(scope, key)
test code
from contextlib import contextmanager
from unittest import mock
# Usually in conftest.py
@contextmanager
def mocked_import(module_name, my_mock=None) -> mock.Mock:
orig_import = __import__
if not my_mock:
my_mock = mock.MagicMock(name=f'mock_{module_name}')
def import_mock(name, *args):
return my_mock if name == module_name else orig_import(name, *args)
with mock.patch('builtins.__import__', side_effect=import_mock):
yield my_mock
# ---------------------------------------------------------------------
# some testcase.py
# fails with "ModuleNotFoundError: No module named 'dbutils'"
def test_read_secrets():
read_secret_from_databricks('scope1', 'key1')
# Passes
def test_read_secrets_with_mock():
with mocked_import('pyspark.dbutils') as m:
read_secret_from_databricks('scope1', 'key1')
assert mock.call.secrets.get('scope1', 'key1') in m.mock_calls