Python: макет модуля без импорта или необходимости его существования - PullRequest
11 голосов
/ 01 марта 2011

Я начинаю использовать python mock library для моего тестирования. Я хочу смоделировать модуль, который импортируется в пространство имен тестируемого модуля, фактически не импортируя его или не требуя, чтобы он существовал первым (то есть выбрасывая ImportError).

Предположим, существует следующий код:

foo.py

 import helpers
 def foo_func():
    return helpers.helper_func()

Цель состоит в том, чтобы протестировать foo_func (), когда 'helpers.py' нигде не существует, и если он существует, действовать так, как будто его нет.

Сначала попробуйте, используя очень крутой декоратор @patch:

from mock import patch, sentinel
import foo
@patch("foo.helpers")
def foo_test(mock):
    mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

Это работает, если модуль "помощников" можно импортировать. Если он не существует, я получаю ImportError.

Следующая попытка с патчем без декоратора:

from mock import patch, sentinel, Mock
import foo
helpers_mock = patch("foo.helpers")
helpers_mock.start()

def foo_test():
    helpers_mock.helper_func = Mock('helper_func')
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

Опять же, это не работает, если "помощники" отсутствуют ... и, если оно существует, утверждение не выполняется. Не совсем уверен, почему это происходит.

Третья попытка, текущее решение:

import sys
helpers_mock = Mock(name="helpers_mock", spec=['helper_func'])
helpers_mock.__name__ = 'helpers'
sys.modules['helpers'] = helpers_mock
import foo
def foo_test():
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

Этот тест проходит независимо от того, существует "helpers.py" или нет.

Это лучший способ достичь этой цели? Предоставляет ли библиотека-издевательство альтернативу этому? Какие еще способы я могу сделать это?

Ответы [ 2 ]

4 голосов
/ 01 марта 2011

Вы упускаете из виду, что такое насмешка. Вы должны создавать их, когда вам нужен объект с определенным интерфейсом, независимо от того, как он реализован.

То, что вы делаете, пытается перестроить систему модулей Python, плюс это довольно ужасное злоупотребление глобальными переменными для загрузки.

Вместо того, чтобы делать foo модулем, создайте класс Foo и передайте помощники в конструкторе.

class Foo(object):
    def __init__(self, helpers):
        self.helpers = helpers

# then, instead of import foo:
foo = Foo(mock_helpers)

Даже если настоящие «помощники» на самом деле будут модулем, нет никаких причин, по которым вам нужно связываться с sys.modules и настраивать его с помощью import в ваших тестах.

И если foo должен быть модулем, это тоже хорошо, но вы делаете это так:

# foo.py
class Foo(object):
    pass # same code as before, plus foo_func

try:
   import whatever
   _singleton = Foo(whatever)
except ImportError:
   _singleton = Foo(something_else)

def foo_func():
   return _singleton.foo_func()

Так работают большие куски стандартной библиотеки. Это в значительной степени стандарт для определения синглтоноподобных модулей.

1 голос
/ 12 февраля 2019

У меня была похожая проблема, когда не удалось загрузить библиотеку helpers, так как для этого требовалось специальное оборудование.Вместо того, чтобы вносить радикальные изменения в код, который вы хотите протестировать, альтернативой является вставка «поддельного» каталога в sys.path, как подсказывает способ добавления пакета в путь sys для тестирования

import os, sys
fake_dir = os.path.join(os.path.dirname(__file__), 'fake')
assert(os.path.exists(fake_dir))
sys.path.insert(0, fake_dir)
import foo
from unittest.mock import sentinel
def foo_test():
    foo.helpers.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

, где fake имеет структуру:

.
├── fake/
│   └── helpers/
│       └── __init__.py
├── foo.py
└── helpers/

и __init__.py имеет

from unittest.mock import Mock
helper_func = Mock()
...