Как издеваться над импортом - PullRequest
113 голосов
/ 28 декабря 2011

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

Фактически, B не установлен в тестовой среде специально.

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

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

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

Ответы [ 6 ]

111 голосов
/ 28 декабря 2011

Вы можете присвоить 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
18 голосов
/ 28 августа 2013

Встроенный __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: Как указано в changelog 3.0, , __builtin__ теперь называется builtins:

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

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

13 голосов
/ 10 мая 2016

Как издеваться над импортом, (издеваться над A.B)?

Модуль 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 голосов
/ 19 августа 2013

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

Я нашел хороший способ для насмешки над импортом в Python.Это найденное Эриком Zaadi решение здесь , которое я просто использую внутри моего Django приложения.

У меня есть класс SeatInterface, который является интерфейсомдо Seat модель класса.Итак, внутри моего seat_interface модуля у меня есть такой импорт:

from ..models import Seat

class SeatInterface(object):
    (...)

Я хотел создать изолированные тесты для класса SeatInterface с фиктивным Seat классом как FakeSeat.Проблема была в том, как запускать тесты в автономном режиме, когда приложение Django не работает.У меня была ошибка ниже:

ImproperlyConfigured: запрошенная настройка 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 с

ОК

3 голосов
/ 28 декабря 2011

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

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

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

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

docs.python.org: __import__

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

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

...