Como simular uma importação

144

O módulo Ainclui import Bna parte superior. No entanto, sob condições de teste gostaria de zombar B em A(simulado A.B) e completamente abster-se de importação B.

De fato, Bnão é instalado no ambiente de teste de propósito.

Aé a unidade em teste. Eu tenho que importar Acom todas as suas funcionalidades. Bé o módulo que eu preciso zombar. Mas como posso zombar por Bdentro Ae parar Ade importar o real B, se a primeira coisa Aque importa é importar B?

(A razão pela qual B não está instalado é que eu uso o pypy para testes rápidos e, infelizmente, B ainda não é compatível com o pypy.)

Como isso poderia ser feito?

Jonathan
fonte

Respostas:

134

Você pode atribuir a sys.modules['B']antes de importar Apara obter o que deseja:

test.py :

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

print(A.B.__name__)

A.py :

import B

Nota B.py não existe, mas ao executar test.pynenhum erro é retornado e é print(A.B.__name__)impresso mock_B. Você ainda precisa criar um local mock_B.pyonde você zomba Bdas funções / variáveis ​​/ etc. Ou você pode apenas atribuir um Mock()diretamente:

test.py :

import sys
sys.modules['B'] = Mock()
import A
Rob Wouters
fonte
3
tenha em mente que Mocknão irá corrigir alguns atributos mágicos ( __%s__) como __name__.
reclosedev
7
@reclosedev - existe Magic Mock para isso #
Jonathan
2
Como você desfaz isso para que a importação B seja novamente um ImportError? Eu tentei, sys.modules['B'] = Nonemas não parece funcionar.
Audiodude 20/08/19
2
Como você redefine essa importação simulada no final do teste, para que outros arquivos de teste de unidade não sejam afetados pelo objeto simulado?
Riya John
1
para maior clareza, você deve editar a resposta para realmente importar mocke ligarmock.Mock()
nmz787
28

O builtin __import__pode ser ridicularizado com a biblioteca 'mock' para mais controle:

# 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

Diga se Aparece com:

import B

def a():
    return B.func()

A.a()retornos b_mock.func()que também podem ser ridicularizados.

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

Nota para Python 3: Conforme declarado no log de alterações do 3.0 , __builtin__agora é nomeado builtins:

Renomeado módulo __builtin__para builtins(removendo os sublinhados, adicionando um 's').

O código nesta resposta funcionará bem se você substituir __builtin__pelo builtinsPython 3.

siebz0r
fonte
1
Alguém confirmou que isso funciona? Estou vendo import_mockser chamado para o import A, mas não para qualquer coisa que importa.
Jonathon Reinhart
3
Com Python 3.4.3 receboImportError: No module named '__builtin__'
Lucas Cimon 4/17
você tem que importar__builtin__
Aidenhjj
1
@LucasCimon, substitua __builtin__por builtinspara python3 ( docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__ )
Lucas Marlin
17

Como zombar de uma importação, (zombar AB)?

O módulo A inclui a importação B na parte superior.

Fácil, basta zombar da biblioteca em sys.modules antes de ser importada:

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

e, desde Aque não dependa de tipos específicos de dados retornados dos objetos de B:

import A

deve funcionar.

Você também pode zombar import A.B:

Isso funciona mesmo se você tiver submódulos, mas você deseja zombar de cada módulo. Digamos que você tenha o seguinte:

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

Para zombar, basta fazer o seguinte antes que o módulo que contém os itens acima seja importado:

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

(Minha experiência: eu tinha uma dependência que funciona em uma plataforma, Windows, mas não funcionava no Linux, onde executamos nossos testes diários. Então, eu precisava zombar da dependência de nossos testes. Felizmente, era uma caixa preta, então Não precisei criar muita interação.)

Zombando de efeitos colaterais

Adendo: Na verdade, eu precisava simular um efeito colateral que levou algum tempo. Então, eu precisava do método de um objeto para dormir por um segundo. Isso funcionaria assim:

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)

E então o código leva algum tempo para ser executado, assim como o método real.

Aaron Hall
fonte
7

Sei que estou um pouco atrasado para a festa aqui, mas aqui está uma maneira um tanto insana de automatizar isso com a mockbiblioteca:

(aqui está um exemplo de uso)

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

A razão pela qual isso é ridiculamente complicado é quando ocorre uma importação, o python basicamente faz isso (por exemplo from herp.derp import foo)

  1. Existe sys.modules['herp']? Caso contrário, importe-o. Se ainda nãoImportError
  2. Existe sys.modules['herp.derp']? Caso contrário, importe-o. Se ainda nãoImportError
  3. Obter atributo foode sys.modules['herp.derp']. OutroImportError
  4. foo = sys.modules['herp.derp'].foo

Existem algumas desvantagens nessa solução hackeada: se algo mais depende de outras coisas no caminho do módulo, esse tipo de coisa é demais. Além disso, isso funciona para coisas que estão sendo importadas em linha, como

def foo():
    import herp.derp

ou

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

A resposta de Aaron Hall funciona para mim. Só quero mencionar uma coisa importante,

se A.pyvocê faz

from B.C.D import E

então test.pyvocê deve zombar de todos os módulos ao longo do caminho, caso contrário, você obtémImportError

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

Encontrei uma boa maneira de zombar das importações em Python. É a solução Zaadi de Eric encontrada aqui, que eu apenas uso dentro do meu aplicativo Django .

Eu tenho classe SeatInterfaceque é interface para Seatclasse de modelo. Então, dentro do meu seat_interfacemódulo, eu tenho essa importação:

from ..models import Seat

class SeatInterface(object):
    (...)

Eu queria criar testes isolados para SeatInterfaceclasse com Seatclasse zombada como FakeSeat. O problema era - como você executa os testes offline, onde o aplicativo Django está inoperante. Eu tive abaixo do erro:

ImproperlyConfigured: configuração solicitada BASE_DIR, mas as configurações não estão definidas. Você deve definir a variável de ambiente DJANGO_SETTINGS_MODULE ou chamar settings.configure () antes de acessar as configurações.

Ran 1 teste em 0.078s

FAILED (erros = 1)

A solução foi:

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

E então teste magicamente executa OK :)

.
Ran 1 teste em 0.002s

Está bem

Hunter_71
fonte
3

Se você faz um, import ModuleBestá realmente chamando o método embutido __import__como:

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

Você pode sobrescrever esse método importando o __builtin__módulo e criando um invólucro em torno do __builtin__.__import__método. Ou você pode jogar com o NullImportergancho do impmódulo. Capturando a exceção e zombando de seu módulo / classe no exceptbloco

Ponteiro para os documentos relevantes:

docs.python.org: __import__

Acessando Internos de Importação com o Módulo imp

Eu espero que isso ajude. Seja ALTAMENTE avisado de que você adentra os perímetros mais misteriosos da programação em python e que a) é sólida a compreensão do que você realmente deseja alcançar eb) a compreensão profunda das implicações é importante.

Don Question
fonte