Jak udawać import

143

Moduł Azawiera import Bu góry. Jednak w warunkach testowych chciałbym mock B w A(makiety A.B) i całkowicie powstrzymać się od importu B.

W rzeczywistości Bnie jest celowo instalowany w środowisku testowym.

Ajest testowaną jednostką. Muszę importować Az całą jego funkcjonalnością. Bjest modułem, który muszę wyszydzić. Ale jak mogę kpić Bwewnątrz Ai przestać Aimportować rzeczywistość B, jeśli pierwszą rzeczą Ajest importowanie B?

(Powodem, dla którego B nie jest zainstalowane, jest to, że używam pypy do szybkiego testowania i niestety B nie jest jeszcze kompatybilny z pypy.)

Jak można to zrobić?

Jonathan
źródło

Odpowiedzi:

134

Możesz przypisać do sys.modules['B']przed importem, Aaby uzyskać to, co chcesz:

test.py :

import sys
sys.modules['B'] = __import__('mock_B')
import A

print(A.B.__name__)

A.py :

import B

Uwaga B.py nie istnieje, ale po uruchomieniu test.pyżaden błąd nie jest zwracany i print(A.B.__name__)drukowany mock_B. Nadal musisz utworzyć miejsce, w mock_B.pyktórym mockujesz Brzeczywiste funkcje / zmienne / itp. Lub możesz po prostu przypisać Mock()bezpośrednio:

test.py :

import sys
sys.modules['B'] = Mock()
import A
Rob Wouters
źródło
3
pamiętaj, że Mocknie poprawi to niektórych magicznych atrybutów ( __%s__), takich jak __name__.
reclosedev
7
@reclosedev - tam Magia Mock za to
Jonathan
2
Jak to cofnąć, aby import B był ponownie importem ImportError? Próbowałem, sys.modules['B'] = Noneale to nie działa.
audiodude
2
Jak zresetować ten udawany import na koniec testu, aby modelowany obiekt nie miał wpływu na inne pliki testów jednostkowych?
Riya John
1
Dla jasności należy edytować odpowiedź, aby faktycznie zaimportować, mocka następnie wywołaćmock.Mock()
nmz787
28

Wbudowaną wersję __import__można mockować za pomocą biblioteki „mock”, aby uzyskać większą kontrolę:

# 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

Powiedz Awygląda tak:

import B

def a():
    return B.func()

A.a()zwroty, b_mock.func()które również można wyszydzić.

b_mock.func.return_value = 'spam'
A.a()  # returns 'spam'

Uwaga dotycząca Pythona 3: Jak stwierdzono w dzienniku zmian w wersji 3.0 , __builtin__ma teraz nazwę builtins:

Zmieniono nazwę modułu __builtin__na builtins(usunięcie podkreślenia, dodanie litery „s”).

Kod w tej odpowiedzi działa dobrze, jeśli zastąpisz __builtin__go builtinsdla Pythona 3.

siebz0r
źródło
1
Czy ktoś potwierdził, że to działa? Widzę, że jestem import_mockwezwany do import A, ale nie do niczego, co importuje.
Jonathon Reinhart
3
Z Pythonem 3.4.3 dostajęImportError: No module named '__builtin__'
Lucas Cimon
musisz importować__builtin__
Aidenhjj
1
@LucasCimon zastąpić __builtin__przez builtinsdla python3 ( docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__ )
Łukaszowy marlinów
17

Jak mockować import (mock AB)?

Moduł A zawiera import B u góry.

Łatwo, po prostu mock bibliotekę w sys.modules, zanim zostanie zaimportowana:

if wrong_platform():
    sys.modules['B'] = mock.MagicMock()

a następnie, o ile Anie polega na określonych typach danych zwracanych z obiektów B:

import A

powinno po prostu działać.

Możesz też kpić z import A.B:

Działa to nawet, jeśli masz podmoduły, ale będziesz chciał mockować każdy moduł. Powiedz, że masz to:

from foo import This, That, andTheOtherThing
from foo.bar import Yada, YadaYada
from foo.baz import Blah, getBlah, boink

Aby wyszydzić, po prostu wykonaj poniższe czynności, zanim zaimportowany zostanie moduł zawierający powyższe:

sys.modules['foo'] = MagicMock()
sys.modules['foo.bar'] = MagicMock()
sys.modules['foo.baz'] = MagicMock()

(Moje doświadczenie: miałem zależność, która działa na jednej platformie, Windows, ale nie działała na Linuksie, gdzie przeprowadzamy nasze codzienne testy. Musiałem więc sfałszować zależność dla naszych testów. Na szczęście była to czarna skrzynka, więc Nie musiałem konfigurować wielu interakcji).

Kpiące efekty uboczne

Dodatek: Właściwie potrzebowałem zasymulować efekt uboczny, który zajął trochę czasu. Więc potrzebowałem metody obiektu, aby zasnąć przez sekundę. To działałoby tak:

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 potem kod zajmuje trochę czasu, tak jak prawdziwa metoda.

Aaron Hall
źródło
7

Zdaję sobie sprawę, że jestem trochę spóźniony na imprezę tutaj, ale oto nieco szalony sposób na zautomatyzowanie tego za pomocą mock biblioteki:

(oto przykład użycia)

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

Powód, dla którego jest to tak absurdalnie skomplikowane, polega na tym, że gdy następuje import, Python w zasadzie to robi (na przykład from herp.derp import foo)

  1. Czy sys.modules['herp']istnieje? W przeciwnym razie zaimportuj. Jeśli nadal nieImportError
  2. Czy sys.modules['herp.derp']istnieje? W przeciwnym razie zaimportuj. Jeśli nadal nieImportError
  3. Uzyskaj atrybut fooo sys.modules['herp.derp']. JeszczeImportError
  4. foo = sys.modules['herp.derp'].foo

To zhakowane rozwiązanie ma pewne wady: jeśli coś innego zależy od innych rzeczy w ścieżce modułu, ten rodzaj go zakręca. Działa to również tylko w przypadku rzeczy importowanych w linii, takich jak

def foo():
    import herp.derp

lub

def foo():
    __import__('herp.derp')
Anthony Sottile
źródło
6

Odpowiedź Aarona Halla mi odpowiada. Chcę tylko wspomnieć o jednej ważnej rzeczy,

jeśli A.pyto robisz

from B.C.D import E

wtedy test.pymusisz kpić z każdego modułu na ścieżce, w przeciwnym razie otrzymaszImportError

sys.modules['B'] = mock.MagicMock()
sys.modules['B.C'] = mock.MagicMock()
sys.modules['B.C.D'] = mock.MagicMock()
Qingyi Wu
źródło
4

Znalazłem dobry sposób na mockowanie importu w Pythonie. To rozwiązanie Erica Zaadi znalezione tutaj, którego używam po prostu w mojej aplikacji Django .

Mam klasę, SeatInterfacektóra jest interfejsem do Seatklasy modelu. Więc w moim seat_interfacemodule mam taki import:

from ..models import Seat

class SeatInterface(object):
    (...)

Chciałem stworzyć izolowane testy dla SeatInterfaceklasy z wyimaginowaną Seatklasą jako FakeSeat. Problem polegał na tym, jak uruchomić testy offline, gdy aplikacja Django nie działa. Miałem poniżej błąd:

Nieprawidłowo skonfigurowany: zażądano ustawienia BASE_DIR, ale ustawienia nie są skonfigurowane. Musisz zdefiniować zmienną środowiskową DJANGO_SETTINGS_MODULE lub wywołać settings.configure () przed uzyskaniem dostępu do ustawień.

Uruchomiono 1 test w 0,078 s

NIEUDANE (błędy = 1)

Rozwiązaniem było:

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

A potem test magicznie działa OK :)

.
Uruchomiono 1 test w 0,002 s

dobrze

Hunter_71
źródło
3

Jeśli to zrobisz import ModuleB, naprawdę wywołujesz wbudowaną metodę __import__jako:

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

Możesz nadpisać tę metodę, importując __builtin__moduł i tworząc opakowanie wokół __builtin__.__import__metody. Lub możesz pobawić się NullImporterhakiem z impmodułu. Przechwytywanie wyjątku i Mockowanie modułu / klasy wexcept Mockowanie -block.

Wskaźnik do odpowiednich dokumentów:

docs.python.org: __import__

Dostęp do wewnętrznych elementów importu za pomocą modułu imp

Mam nadzieję, że to pomoże. Bądź BARDZO doradzony, abyś wkroczył w bardziej tajemnicze granice programowania w Pythonie i że a) dokładne zrozumienie tego, co naprawdę chcesz osiągnąć, oraz b) dokładne zrozumienie konsekwencji jest ważne.

Don Pytanie
źródło