Частная функция Python Monkey patch

У меня есть модуль с функцией (назовите ее a()), которая вызывает другую функцию, определенную в том же модуле (назовите ее __b()). __b() - это функция, которая обращается к веб-сайту через urllib2 и возвращает некоторые данные. Сейчас я пытаюсь протестировать a(), но, конечно, не хочу, чтобы мои модульные тесты общались в Интернете. Таким образом, я думаю, что если я смогу обезьяно запатчить __b() с помощью функции, которая возвращает стандартные данные, тогда я смогу написать тесты для a().

Если быть более конкретным, мой модуль выглядит примерно так:

def a():
    return __b("someval")

def __b(args):
    return something_complex_with_args

Итак, теперь я хочу протестировать a(), но мне нужно исправить __b. Проблема в том, что A) подавляющее большинство информации об исправлении monkey применяется к методам класса, а не к функциям в модуле, и B) функция, которую я хочу исправить, является частной. Я готов изменить __b на не конфиденциальный, если это сделает процесс более осуществимым, но не хочу.

Предложения?

Изменить: в нынешнем виде тестовый класс выглядит так:

from unittest import TestCase

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule._b = newfn

class TestMyModule(TestCase):
    def test_basic(self):
        print(mymodule.a('somearg'))

И когда я запускаю это, я вижу результат, если исправление обезьяны не было выполнено вообще, вместо того, чтобы видеть, как {'a': 'b'} распечатывается.


person Adam Parkin    schedule 26.12.2012    source источник
comment
В общем, если чему-либо вне модуля требуется доступ к функции (включая тесты), вам, вероятно, не следует использовать формат __name. Вместо этого используйте формат _name, чтобы обозначить, что это внутренняя функция, при этом позволяя тем вещам, которые действительно нуждаются в доступе, получить к ней доступ.   -  person Amber    schedule 27.12.2012
comment
Изменение имени (__x) - это не то же самое, что private, и если вы точно не знаете, что это такое, вам, вероятно, не следует его использовать. В Python нет сокрытия данных.   -  person Gareth Latty    schedule 27.12.2012
comment
@Lattyware: я знаю, что это не то же самое, что частное, но это так близко, насколько это возможно. Почему есть опасения, что он будет назван с двойным подчеркиванием?   -  person Adam Parkin    schedule 27.12.2012
comment
@AdamParkin Поскольку добульное подчеркивание обеспечивает искажение имени - имя изменяется (посмотрите в документации, почему и что именно происходит). Вам не нужно делать что-то частным в Python. Ваша проблема - очень ясный пример того, почему это плохая идея. Python - это не Java или C ++, и написание его так, как будто он есть, приведет к плохому коду.   -  person Gareth Latty    schedule 27.12.2012
comment
Под документами вы имеете в виду (docs.python.org/2/tutorial /classes.html#tut-private)? Так что я до сих пор не понимаю, почему это вообще плохо. Возможно, переместите это в чат, поскольку это не имеет отношения к моему вопросу?   -  person Adam Parkin    schedule 28.12.2012


Ответы (3)


Кажется, я не могу воспроизвести вашу проблему (я немного изменил ваш пример, поскольку он вообще не работает как есть). Вы что-то неправильно набрали (например, mymodule._b вместо mymodule.__b)?

mymodule.py:

def a(x):
    return __b("someval")

def __b(args):
    return "complex_thingy: {}".format(args)

mytest.py:

from unittest import TestCase

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule.__b = newfn

class TestMyModule(TestCase):
    def test_basic(self):
        print(mymodule.a('somearg'))

Вывод:

C:\TEMP>python -m unittest mytest
{'a': 'b'}
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

C:\TEMP>

Кажется, работает нормально.


Или вне unittest:

mytest2.py:

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule.__b = newfn

print(mymodule.a('somearg'))

Вывод:

C:\TEMP>python mytest2.py
{'a': 'b'}

C:\TEMP>
person Gerrat    schedule 19.06.2013

Если ваш модуль был назван 'foo', тогда должно работать следующее.

import foo

def patched_version():
    return 'Hello'

foo.__b = patched_version

print (foo.a())

где foo.py

def a():
    return __b()

def __b():
    return 'Goodbye'
person jeffknupp    schedule 26.12.2012
comment
Нет, исходная версия __b () все еще вызывается (независимо от того, __b или _b). - person Adam Parkin; 27.12.2012
comment
Если посмотреть на это немного подробнее, ваш ответ работает, но когда я изменяю печать, чтобы она находилась внутри тестового метода (как в моем вопросе), вызывается исходная версия __b. - person Adam Parkin; 27.12.2012
comment
Это потому, что unittest выполняется совсем не так, как «нормальный» Python. Попробуйте поместить foo.__b = patched_version в setUp функцию для вашего теста. Если вы используете что-то другое, кроме unittest, YMMV. - person jeffknupp; 28.12.2012
comment
Я использую unittest, и перемещение его в setUp не решило проблему. - person Adam Parkin; 28.12.2012

Я столкнулся с той же проблемой, но наконец нашел решение. Проблема была при использовании патча обезьяны в классе unittest.TestCase. Вот решение, которое отлично работает:

Если вы используете Python 2, вам необходимо установить «фиктивную» библиотеку (http://www.voidspace.org.uk/python/mock/) с помощью easy_install или каким-либо другим способом. Эта библиотека уже входит в состав Python 3.

Вот как выглядит код:

from mock import patch

    class TestMyModule(TestCase):
        def test_basic(self):
            with patch('mymodule._b') as mock:
                mock.return_value={"a" : "b"} # put here what you want the mock function to return. You can make multiple tests varying these values.
                #keep the indentation. Determines the scope for the patch.
                print(mymodule.a('somearg'))

Хотя этот способ, по-видимому, кажется немного менее удобным по сравнению с созданием имитационной функции, где мы могли бы имитировать фактическую подфункцию _b (), имея логику для возврата разных значений на основе разных аргументов, но тогда мы без необходимости добавляем больше шансов на ошибку. В этом подходе мы просто жестко кодируем то, что мы хотим, чтобы наша фиктивная функция возвращала, и тестируем фактическую функцию, которую мы намеревались тестировать, то есть ().

person Afzal Naushahi    schedule 22.01.2013
comment
Нет, у меня все еще не работает. Я получаю: AttributeError: <module 'mymodule' from 'mymodule/__init__.py'> does not have the attribute '_b' Добавление частной функции в init .py устраняет ошибку, но исправление не имело никакого эффекта (исходная версия все еще вызывалась). - person Adam Parkin; 27.01.2013