Як знущатися над імпортом


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 - є Чарівний макет для цього
Джонатан

2
Як ви скасуєте це, щоб імпорт B знову був ImportError? Я спробував, sys.modules['B'] = Noneале, здається, не виходить.
audiodude

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.


1
Хто-небудь підтвердив це працює? Я бачу, import_mockяк мене викликають import A, але не за все, що імпортує.
Джонатан Райнхарт

3
З Python 3.4.3 я отримуюImportError: No module named '__builtin__'
Лукас Кімон

вам потрібно імпортувати__builtin__
Айденхдж

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 с

FAILED (помилки = 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

Якщо ви import ModuleBдійсно називаєте вбудований метод __import__так:

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

Ви можете перезаписати цей метод, імпортуючи __builtin__модуль і зробивши обгортку навколо __builtin__.__import__методу. Або ви могли пограти з NullImporterгачком з impмодуля. Ловіть виняток і знущайтеся над своїм модулем / класом у програміexceptЛовіть -блоку.

Вказівник на відповідні документи:

docs.python.org: __import__

Доступ до внутрішніх даних імпорту за допомогою модуля імпульсу

Я сподіваюся, що це допомагає. Будьте ЧИСТО застережені, що ви переходите до більш прихованих периметрів програмування python і що a) чітке розуміння того, чого ви дійсно хочете досягти, і b) важливе глибоке розуміння наслідків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.