Атрибут Mocking для изменения при каждом обращении к нему

#python #unit-testing #mocking

#python #модульное тестирование #насмешливый

Вопрос:

Вот функция, которую я хочу протестировать:

 def send_something():
    conn = lib.conn.SSH1
    conn.open()
    conn.send('lsn')
    if 'home' not in conn.recbuf:
        return lib.FAIL

    conn.send('whoamin')
    if USER not in conn.recbuf:
        return lib.FAIL
    return lib.PASS
  

Каждый раз, когда я вызываю conn.send(), результат вызова сохраняется в conn.recbuf. Чтобы проверить это, мне нужно, чтобы conn.recbuf отличался при каждом его вызове. (Я знаю, что мог бы просто изменить conn.recbuf на строку, содержащую как home, так и USER, но это не будет работать для более сложных функций)

Вот что я пока придумал в своем тесте:

 @mock.patch.object(demo_sequence.lib, 'conn')
def test_send_something(mock_conn):
    mock_conn.SSH1.recbuf = 'home'
    assert demo_sequence.lib.PASS == demo_sequence.send_something()
    mock_conn.SSH1.send.assert_any_call('lsn')
    mock_conn.SSH1.send.assert_any_call('whoamin')
  

Очевидно, что это не удается, потому что conn.recbuf является только ‘home’ и не содержит USER.

Мне нужно что-то вроде conn.recbuf.side_effect = [‘home’, USER], но он будет работать, если просто ссылаться на recbuf, а не вызывать его.

Комментарии:

1. Разве вы не хотели бы просто издеваться send , чтобы он не делал ничего, кроме обновления recbuff ? Трудно предоставить хорошее решение, не зная, какой тип ` conn` на самом деле.

2. Библиотека conn странная (плохая). SSH1 также должен быть изменен, потому что он не всегда существует. Вот почему я выбрал mock в качестве уровня lib.conn.

3. Я мог бы также порекомендовать вместо жесткого кодирования значения conn in send_something сделать его аргументом функции. Тогда вы можете передать любой объект, который вы хотите, в качестве аргумента вместо того, чтобы использовать mock.patch вообще.

4. Если вы проводите модульное тестирование send_something, то, вероятно, лучше полностью имитировать conn. Также возможно выполнить другой, особый вид теста для библиотеки lib.conn, если вы хотите убедиться, что он соответствует контракту. Кроме того, можно создать оболочку / адаптер вокруг «плохой» библиотеки — если ваш код использует библиотеку во многих местах.

Ответ №1:

Если вы можете изменить conn.recbuf на свойство, вы могли бы использовать PropertyMock для достижения желаемого эффекта.

 from unittest import mock

class Dummy:

    def __init__(self, myattribute):
        self._myattribute = myattribute

    @property
    def myattribute(self):
        return self._myattribute

def test():
    with mock.patch('__main__.Dummy.myattribute', new_callable=mock.PropertyMock) as mocked_attribute:
        mocked_attribute.side_effect = [4,5,6]
        d = Dummy("foo")
        print(d.myattribute)
        print(d.myattribute)
        print(d.myattribute)
  

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

Комментарии:

1. Этот подход действительно удовлетворяет мои потребности. Я видел несколько комментариев и предыдущих ответов, в которых говорилось об использовании PropertyMock, но я никогда не проверял, является ли recbuf реальным свойством. (имеет свойство decorator). Спасибо за дальнейшее объяснение.

Ответ №2:

То, что вы ищете, это side_effect :https://docs.python.org/3/library/unittest.mock.html

Допустим, mock это ваш макет объекта. Затем вы можете сделать это:

 mock.side_effect = [a,b,c]
  

затем при каждом вызове mock возвращается следующее значение из списка.

 mock() # -> a
mock() # -> b
mock() # -> c
  

Комментарии:

1. Отклонено, потому что это именно то, что still learning упомянул в своем последнем предложении, а также объяснил, почему это не работает: ему нужно именно такое поведение для атрибутов, а не для вызываемых объектов.

2. Понял. Вы можете добиться того же для атрибутов, издеваясь над __getattr__ методом, но это может быть немного запутанным.

3. Можете ли вы подробнее рассказать о mocking getattr для достижения решения моей проблемы?

4. Когда вы выполняете a.x, он преобразуется в a.__getattr__(‘x’). Итак, вы можете попробовать издеваться над этим.

Ответ №3:

Ранее я сталкивался с аналогичной проблемой и нашел решение с помощью PropertyMock . В моем случае я хотел сохранить значение в другом месте тестового примера для утверждения, вызывая метод через side_effect , когда свойство было установлено для класса, над которым я издевался.

Я думаю, что это должно сработать и в вашем случае (если я понял, что вы пытаетесь сделать).

Решение состоит в том, чтобы использовать PropertyMock для вашего свойства recbuf и поместить side_effect туда:

 from unittest.mock import MagicMock, PropertyMock

conn = MagicMock()
type(conn).recbuf = PropertyMock(side_effect=['home', USER])
  

Побочный эффект будет вызываться при каждом обращении к свойству recbuf , в данном случае повторяясь ['home', USER] каждый раз. Я думаю, вам также нужно будет исправить это USER значение, чтобы вы не получали ошибку имени.

Надеюсь, это сработает / поможет!