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

144

Модуль Aвключает import Bв себя в верхней части. Однако в условиях испытания , я хотел бы, чтобы дразнить B в A(макет A.B) и полностью отказаться от импорта B.

На самом деле, Bне установлен в тестовой среде специально.

Aэто тестируемое устройство. Я должен импортировать Aсо всеми его функциями. Bэто модуль, который мне нужно издеваться Но как я могу издеваться Bвнутри Aи перестать Aимпортировать реальное B, если первым делом Aэто импорт B?

(Причина, по которой B не установлен, заключается в том, что я использую pypy для быстрого тестирования, и, к сожалению, B еще не совместим с pypy.)

Как это можно сделать?

Джонатан
источник

Ответы:

134

Вы можете назначить 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
Роб Ваутерс
источник
3
Имейте в виду, что Mockне будет исправлять некоторые магические атрибуты ( __%s__), как __name__.
reclosedev
7
@reclosedev - есть магия Mock для этого
Джонатан
2
Как отменить это, чтобы импорт B снова был ImportError? Я пытался, sys.modules['B'] = Noneно это не сработало.
аудиодуд
2
Как сбросить этот фиктивный импорт в конце теста, чтобы другие файлы юнит-теста не были затронуты фиктивным объектом?
Рия Джон
1
для ясности, вы должны отредактировать ответ для фактического импорта, mockа затем позвонитьmock.Mock()
nmz787
28

Встроенный модуль __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'

Примечание для Python 3: Как указано в журнале изменений для 3.0 , __builtin__теперь он называется builtins:

Переименован модуль __builtin__в builtins(удаление подчеркивания, добавление 's').

Код в этом ответе работает нормально , если заменить __builtin__на builtinsна Python 3.

siebz0r
источник
1
Кто-нибудь подтвердил, что это работает? Я вижу, что меня import_mockзовут import A, но не за что-то импортируемое.
Джонатон Рейнхарт
3
С Python 3.4.3 я получаюImportError: No module named '__builtin__'
Лукас Кимон
Вы должны импортировать__builtin__
Aidenhjj
1
@LucasCimon, заменить __builtin__на builtinsдля Python3 ( docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__ )
Luke Marlin
17

Как издеваться над импортом, (издеваться над 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)

И затем выполнение кода занимает некоторое время, как настоящий метод.

Аарон Холл
источник
7

Я понимаю, что немного опоздал на вечеринку здесь, но вот несколько безумный способ автоматизировать это с 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')
Энтони Соттиль
источник
7

Ответ Аарона Холла работает на меня. Просто хочу упомянуть одну важную вещь,

если в 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()
Циньи Ву
источник
4

Я нашел хороший способ издеваться над импортом в 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 с

хорошо

Hunter_71
источник
3

Если вы делаете, import ModuleBвы действительно вызываете встроенный метод __import__как:

ModuleB = __import__('ModuleB', globals(), locals(), [], -1)

Вы можете перезаписать этот метод, импортировав __builtin__модуль и сделав обертку вокруг __builtin__.__import__метода. Или вы можете играть с NullImporterкрючком из impмодуля. Перехват исключения и макет вашего модуля / класса в except-блоке.

Указатель на соответствующие документы:

docs.python.org: __import__

Доступ к внутренним объектам импорта с помощью модуля Imp

Надеюсь, это поможет. Будьте ДАЛЕКО посоветовал , что вы вступаете в более тайные периметры питона программирования и что а) четкое понимание того, что вы действительно хотите достичь , и б) полное понимание последствий имеет важное значение.

Дон Вопрос
источник