Как издеваться над импортом

Модуль 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)

  1. Есть ли sys.modules['herp'] существовать? Остальное импортируй. Если все еще нет ImportError
  2. Есть ли sys.modules['herp.derp'] существовать? Остальное импортируй. Если все еще нет ImportError
  3. Получить атрибут foo из sys.modules['herp.derp'], еще ImportError
  4. 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 предпочтет его исходному модулю.

  1. Создайте замещающий модуль в подкаталоге, например:

            mkdir -p test/mocked-lib
    ${EDITOR} test/mocked-lib/B.py
    
  2. До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
Другие вопросы по тегам