Comment se moquer d'une importation

144

Le module Acomprend import Bà son sommet. Cependant , dans des conditions de test , je voudrais se moquer B de A(faux A.B) et de ne pas tout à fait d'importer B.

En fait, Bn'est pas installé exprès dans l'environnement de test.

Aest l'unité sous test. Je dois importer Aavec toutes ses fonctionnalités. Best le module dont je dois me moquer. Mais comment puis-je me moquer de l' Bintérieur Aet arrêter Ad'importer le réel B, si la première chose à Afaire est l'importation B?

(La raison pour laquelle B n'est pas installé est que j'utilise pypy pour des tests rapides et malheureusement B n'est pas encore compatible avec pypy.)

Comment cela pourrait-il être fait?

Jonathan
la source

Réponses:

134

Vous pouvez attribuer à sys.modules['B']avant d'importer Apour obtenir ce que vous voulez:

test.py :

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

print(A.B.__name__)

A.py :

import B

Remarque B.py n'existe pas, mais lors de l'exécution, test.pyaucune erreur n'est renvoyée et print(A.B.__name__)s'imprime mock_B. Vous devez toujours créer un mock_B.pyemplacement où vous vous moquez Bdes fonctions / variables / etc. Ou vous pouvez simplement attribuer un Mock()directement:

test.py :

import sys
sys.modules['B'] = Mock()
import A
Rob Wouters
la source
3
gardez à l'esprit que Mockcertains attributs magiques ( __%s__) comme __name__.
reclosedev
7
@reclosedev - il y a Magic Mock pour ça
Jonathan
2
Comment annuler cela pour que l'importation B soit à nouveau une ImportError? J'ai essayé sys.modules['B'] = Nonemais cela ne semble pas fonctionner.
audiodude
2
Comment réinitialiser cette importation simulée à la fin du test, afin que les autres fichiers de test unitaire ne soient pas affectés par l'objet simulé?
Riya John le
1
pour plus de clarté, vous devez modifier la réponse pour importer réellement mock, puis appelermock.Mock()
nmz787
28

L'intégré __import__peut être simulé avec la bibliothèque 'mock' pour plus de contrôle:

# 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

Dites Aressemble à:

import B

def a():
    return B.func()

A.a()retours b_mock.func()qui peuvent être moqués également.

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

Remarque pour Python 3: comme indiqué dans le journal des modifications de la version 3.0 , __builtin__est maintenant nommé builtins:

Module renommé __builtin__en builtins(suppression des traits de soulignement, ajout d'un «s»).

Le code de cette réponse fonctionne bien si vous remplacez __builtin__par builtinspour Python 3.

siebz0r
la source
1
Quelqu'un at-il confirmé que cela fonctionne? Je vois import_mockêtre appelé pour le import A, mais pas pour tout ce qu'il importe.
Jonathon Reinhart
3
Avec Python 3.4.3 je reçoisImportError: No module named '__builtin__'
Lucas Cimon
vous devez importer__builtin__
Aidenhjj
1
@LucasCimon, remplacez __builtin__par builtinspour python3 ( docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__ )
Luke Marlin
17

Comment se moquer d'une importation, (mock AB)?

Le module A inclut l'importation B en son sommet.

Facile, simulez simplement la bibliothèque dans sys.modules avant qu'elle ne soit importée:

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

et puis, tant que Ane repose pas sur des types spécifiques de données renvoyées par les objets de B:

import A

devrait juste fonctionner.

Vous pouvez également vous moquer import A.B:

Cela fonctionne même si vous avez des sous-modules, mais vous voudrez vous moquer de chaque module. Dites que vous avez ceci:

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

Pour simuler, faites simplement ce qui suit avant que le module qui contient ce qui précède ne soit importé:

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

(Mon expérience: j'avais une dépendance qui fonctionne sur une plate-forme, Windows, mais qui ne fonctionnait pas sous Linux, où nous exécutons nos tests quotidiens. J'ai donc dû me moquer de la dépendance pour nos tests. Heureusement, c'était une boîte noire, donc Je n'ai pas eu besoin de mettre en place beaucoup d'interaction.)

Effets secondaires moqueurs

Addendum: En fait, j'avais besoin de simuler un effet secondaire qui prenait du temps. J'avais donc besoin de la méthode d'un objet pour dormir une seconde. Cela fonctionnerait comme ceci:

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)

Et puis le code prend un certain temps à s'exécuter, tout comme la vraie méthode.

Salle Aaron
la source
7

Je me rends compte que je suis un peu en retard à la fête ici, mais voici une façon un peu insensée d'automatiser cela avec la mockbibliothèque:

(voici un exemple d'utilisation)

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

La raison pour laquelle cela est si ridiculement compliqué est lorsqu'une importation se produit, python le fait essentiellement (prenez par exemple from herp.derp import foo)

  1. sys.modules['herp']Existe- t- il? Sinon, importez-le. Si toujours pasImportError
  2. sys.modules['herp.derp']Existe- t- il? Sinon, importez-le. Si toujours pasImportError
  3. Obtenez l'attribut foode sys.modules['herp.derp']. AutreImportError
  4. foo = sys.modules['herp.derp'].foo

Il y a quelques inconvénients à cette solution piratée ensemble: si quelque chose d'autre repose sur d'autres éléments dans le chemin du module, ce genre de problème. De plus, cela ne fonctionne que pour les éléments importés en ligne, tels que

def foo():
    import herp.derp

ou

def foo():
    __import__('herp.derp')
Anthony Sottile
la source
7

La réponse d'Aaron Hall fonctionne pour moi. Je veux juste mentionner une chose importante,

si en A.pytoi

from B.C.D import E

alors dans test.pyvous devez vous moquer de chaque module le long du chemin, sinon vous obtenezImportError

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

J'ai trouvé un bon moyen de me moquer des importations en Python. C'est la solution Zaadi d' Eric trouvée ici que je viens d'utiliser dans mon application Django .

J'ai une classe SeatInterfacequi est l'interface avec la Seatclasse de modèle. Donc, dans mon seat_interfacemodule, j'ai une telle importation:

from ..models import Seat

class SeatInterface(object):
    (...)

Je voulais créer des tests isolés pour la SeatInterfaceclasse avec une Seatclasse simulée comme FakeSeat. Le problème était - comment exécuter les tests hors ligne, là où l'application Django est en panne. J'ai eu une erreur ci-dessous:

ImproperlyConfigured: paramètre demandé BASE_DIR, mais les paramètres ne sont pas configurés. Vous devez soit définir la variable d'environnement DJANGO_SETTINGS_MODULE, soit appeler settings.configure () avant d'accéder aux paramètres.

Test effectué 1 en 0,078 s

ÉCHEC (erreurs = 1)

La solution était:

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

Et puis le test fonctionne comme par magie OK :)

.
Test effectué 1 en 0,002 s

D'accord

Hunter_71
la source
3

Si vous faites un, import ModuleBvous appelez vraiment la méthode intégrée __import__comme suit :

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

Vous pouvez écraser cette méthode en important le __builtin__module et créer un wrapper autour de la __builtin__.__import__méthode. Ou vous pouvez jouer avec le NullImportercrochet du impmodule. Attrapez l'exception et simulez votre module / classe dans le exceptbloc-.

Pointeur vers les documents pertinents:

docs.python.org: __import__

Accéder aux composants internes d'importation avec le module imp

J'espère que ça aide. Soyez vivement conseillé d'entrer dans les périmètres les plus obscurs de la programmation python et que a) une solide compréhension de ce que vous voulez vraiment réaliser et b) une compréhension approfondie des implications est importante.

Don Question
la source