Wie man einen Import verspottet

143

Modul Aenthält import Boben. Allerdings unter Testbedingungen würde Ich mag verspotten B in A(Mock A.B) und vollständig verzichten zu importieren B.

Tatsächlich wird Bes nicht absichtlich in der Testumgebung installiert.

Aist das zu testende Gerät. Ich muss Amit all seiner Funktionalität importieren . Bist das Modul, das ich verspotten muss. Aber wie kann ich verspotten Binnerhalb Aund Stopp Avon der realen importieren B, wenn das erste , was Atut Import ist B?

(Der Grund, warum B nicht installiert ist, ist, dass ich Pypy für schnelle Tests verwende und B leider noch nicht mit Pypy kompatibel ist.)

Wie könnte das gemacht werden?

Jonathan
quelle

Antworten:

134

Sie können sys.modules['B']vor dem Import zuweisen, Aum das zu erhalten, was Sie möchten:

test.py :

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

print(A.B.__name__)

A.py :

import B

Hinweis B.py ist nicht vorhanden, aber beim Ausführen wird test.pykein Fehler zurückgegeben und print(A.B.__name__)gedruckt mock_B. Sie müssen noch eine erstellen, mock_B.pyin der Sie Bdie tatsächlichen Funktionen / Variablen / etc. Verspotten. Oder Sie können einfach eine Mock()direkt zuweisen :

test.py :

import sys
sys.modules['B'] = Mock()
import A
Rob Wouters
quelle
3
Denken Sie daran, dass Mockeinige magische Attribute ( __%s__) wie nicht gepatcht werden __name__.
reclosedev
7
@reclosedev - es gibt Magic Mock dafür
Jonathan
2
Wie machen Sie dies rückgängig, damit der B-Import wieder ein ImportError ist? Ich habe es versucht, sys.modules['B'] = Noneaber es scheint nicht zu funktionieren.
Audiodude
2
Wie können Sie diesen verspotteten Import am Ende des Tests zurücksetzen, damit andere Komponententestdateien nicht vom verspotteten Objekt betroffen werden?
Riya John
1
Aus Gründen der Übersichtlichkeit sollten Sie die Antwort bearbeiten, um sie tatsächlich zu importieren, mockund dannmock.Mock()
nmz787
28

Das eingebaute __import__kann für mehr Kontrolle mit der ' Mock' -Bibliothek verspottet werden:

# 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

Sprich Asieht aus wie:

import B

def a():
    return B.func()

A.a()Rückgaben, b_mock.func()die auch verspottet werden können.

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

Hinweis für Python 3: Wie im Änderungsprotokoll für 3.0 angegeben , __builtin__heißt es jetzt builtins:

Modul umbenannt __builtin__in builtins(Entfernen der Unterstriche, Hinzufügen eines 's').

Der Code in dieser Antwort funktioniert einwandfrei, wenn Sie Python 3 __builtin__durch ersetzen builtins.

siebz0r
quelle
1
Hat jemand bestätigt, dass dies funktioniert? Ich sehe, dass import_mockich für das gerufen werde import A, aber nicht für irgendetwas, das es importiert.
Jonathon Reinhart
3
Mit Python 3.4.3 bekomme ichImportError: No module named '__builtin__'
Lucas Cimon
Sie müssen importieren__builtin__
Aidenhjj
1
@ LucasCimon, ersetzen __builtin__durch builtinsfür Python3 ( docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__ )
Luke Marlin
17

Wie verspottet man einen Import (verspottet AB)?

Modul A enthält oben Import B.

Einfach, verspotten Sie einfach die Bibliothek in sys.modules, bevor sie importiert wird:

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

und dann, solange Anicht bestimmte Datentypen von Bs Objekten zurückgegeben werden:

import A

sollte einfach funktionieren.

Sie können auch verspotten import A.B:

Dies funktioniert auch, wenn Sie Submodule haben, aber Sie möchten jedes Modul verspotten. Angenommen, Sie haben Folgendes:

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

Führen Sie zum Verspotten einfach die folgenden Schritte aus, bevor das Modul mit den oben genannten Elementen importiert wird:

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

(Meine Erfahrung: Ich hatte eine Abhängigkeit, die auf einer Plattform, Windows, funktioniert, aber nicht unter Linux, wo wir unsere täglichen Tests ausführen. Also musste ich die Abhängigkeit für unsere Tests verspotten. Zum Glück war es eine Black Box Ich musste nicht viel Interaktion einrichten.)

Verspottende Nebenwirkungen

Nachtrag: Eigentlich musste ich einen Nebeneffekt simulieren, der einige Zeit in Anspruch nahm. Also brauchte ich die Methode eines Objekts, um eine Sekunde lang zu schlafen. Das würde so funktionieren:

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)

Und dann dauert es einige Zeit, bis der Code ausgeführt wird, genau wie bei der eigentlichen Methode.

Aaron Hall
quelle
7

Mir ist klar, dass ich hier etwas zu spät zur Party komme, aber hier ist eine etwas verrückte Möglichkeit, dies mit der mockBibliothek zu automatisieren :

(hier ist ein Beispiel für die Verwendung)

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

Der Grund, warum dies so lächerlich kompliziert ist, ist, wenn Python importiert wird, dass Python dies im Grunde tut (zum Beispiel from herp.derp import foo)

  1. Existiert sys.modules['herp']? Andernfalls importieren Sie es. Wenn immer noch nichtImportError
  2. Existiert sys.modules['herp.derp']? Andernfalls importieren Sie es. Wenn immer noch nichtImportError
  3. Attribut foovon abrufen sys.modules['herp.derp']. SonstImportError
  4. foo = sys.modules['herp.derp'].foo

Diese gemeinsam gehackte Lösung hat einige Nachteile: Wenn sich etwas anderes auf andere Dinge im Modulpfad stützt, wird diese Art von Fehler behoben. Dies funktioniert auch nur für Dinge, die inline importiert werden, wie z

def foo():
    import herp.derp

oder

def foo():
    __import__('herp.derp')
Anthony Sottile
quelle
6

Aaron Halls Antwort funktioniert für mich. Ich möchte nur eine wichtige Sache erwähnen:

wenn in A.pydir

from B.C.D import E

dann test.pymüssen Sie in jedem Modul entlang des Pfades verspotten, sonst bekommen SieImportError

sys.modules['B'] = mock.MagicMock()
sys.modules['B.C'] = mock.MagicMock()
sys.modules['B.C.D'] = mock.MagicMock()
Qingyi Wu
quelle
4

Ich habe einen guten Weg gefunden, die Importe in Python zu verspotten. Es ist Erics Zaadi- Lösung, die ich hier in meiner Django- Anwendung verwende.

Ich habe eine Klasse, SeatInterfacedie Schnittstelle zur SeatModellklasse ist. Also habe seat_interfaceich in meinem Modul einen solchen Import:

from ..models import Seat

class SeatInterface(object):
    (...)

Ich wollte isolierte Tests für die SeatInterfaceKlasse mit verspotteter SeatKlasse als erstellen FakeSeat. Das Problem war - wie man Tests offline ausführt, wenn die Django-Anwendung nicht verfügbar ist. Ich hatte unten Fehler:

Unsachgemäß konfiguriert: Angeforderte Einstellung BASE_DIR, Einstellungen sind jedoch nicht konfiguriert. Sie müssen entweder die Umgebungsvariable DJANGO_SETTINGS_MODULE definieren oder settings.configure () aufrufen, bevor Sie auf Einstellungen zugreifen können.

Lief 1 Test in 0,078 s

FEHLGESCHLAGEN (Fehler = 1)

Die Lösung war:

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

Und dann läuft der Test magisch OK :)

.
Lief 1 Test in 0,002 s

OK

Hunter_71
quelle
3

Wenn Sie eine machen import ModuleB, rufen Sie die eingebaute Methode wirklich auf __import__als:

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

Sie können diese Methode überschreiben, indem Sie das __builtin__Modul importieren und einen Wrapper um die __builtin__.__import__Methode erstellen . Oder Sie könnten mit dem NullImporterHaken aus dem impModul spielen. Fangen Sie die Ausnahme ab und verspotten Sie Ihr Modul / Ihre Klasse in derexcept Block.

Zeiger auf die entsprechenden Dokumente:

docs.python.org: __import__

Zugriff auf Import-Interna mit dem imp-Modul

Ich hoffe das hilft. Seien Sie sehr darauf hingewiesen, dass Sie in die geheimnisvolleren Grenzen der Python-Programmierung eintreten und dass a) ein solides Verständnis dessen, was Sie wirklich erreichen möchten, und b) ein gründliches Verständnis der Auswirkungen wichtig sind.

Don Frage
quelle