Python — создать макет теста для метода класса с диспетчером контекста

Я пытаюсь написать модульный тест для метода функции класса, который имеет менеджер контекста и много вызовов. Мне трудно понять, как правильно издеваться над функцией, чтобы я мог проверить возвращаемое значение. Класс, который я пытаюсь издеваться, это db. Как вы можете видеть ниже, я использую патч, но я не могу понять, как заставить его возвращать правильный вызов метода. Я получаю общую фиктивную функцию вместо ожидаемого возвращаемого значения.

db_class.py

import db

class Foo():
    def __init__(self):
        pass
    def method(self):
        with db.a() as a:
            b = a.b
            return b.fetch()

unit_db.py

 from mock import Mock, patch, MagicMock
 from db_class import Foo

 @patch('db_class.db')
 def test(db_mock):
     expected_result = [5,10]
     db_mock.return_value = Mock(__enter__ = db_mock,
                                 __exit___ = Mock(),
                                 b = Mock(fetch=expected_result))

     foo = Foo()
     result = foo.method()
     assert result == expected_result

person simple liquids    schedule 25.05.2015    source источник
comment
что именно вы пытаетесь издеваться?   -  person vks    schedule 25.05.2015
comment
Не могли бы вы проверить свой код? Foo не является частью db, а db_class согласно тому, что вы написали. Также кажется, что вы пытаетесь смоделировать весь модуль db, но внутри вашего оператора with вам нужно смоделировать метод db.a(). Наконец, вы неправильно поняли, как указывать методы в макете. Здесь enter и exit являются атрибутами, но должны быть методами. То же самое для выборки в спецификации b.   -  person Cilyan    schedule 25.05.2015
comment
@Cilyan - Спасибо за ответ - я обновил код, чтобы метод Foo импортировался правильно. Ваш ответ имеет большой смысл, я думаю, сначала мне следует добавить еще один патч @patch('db_class.db.a'), чтобы он издевались. Я все еще не понимаю, как реализовать fetch, enter и exit как методы, а не атрибуты.   -  person simple liquids    schedule 25.05.2015
comment
@vks Я пытаюсь издеваться над методом db_class foo.method. Я хочу проверить, что результат, который он возвращает, является ожидаемым результатом.   -  person simple liquids    schedule 25.05.2015
comment
@Cilyan Хорошо, я думаю, что у меня есть решение, спасибо за ваши идеи!   -  person simple liquids    schedule 25.05.2015
comment
@simpleliquids Рад, что это помогло :)   -  person Cilyan    schedule 26.05.2015


Ответы (2)


Благодаря комментаторам я нашел решение, которое работает для меня. Хитрость заключалась в том, чтобы исправить правильный класс, в данном случае я хотел исправить db_class.db.a вместо db_class.db. После этого важно убедиться, что вызов fetch() является методом (думаю, я правильно понял). Сложная часть этой проблемы для меня заключалась в том, чтобы исправить правильную вещь, а также иметь дело с менеджером контекста, который требует немного дополнительной работы.

@patch('db_class.db.a')
def test(db_a):
    expected_result = [5,10]
    b_fetch = MagicMock()
    b_fetch.fetch.return_value = expected_result 
    db_a.return_value = Mock(b = b_fetch,
                         __enter__= db_a,
                         __exit__ =Mock())
    foo = Foo()
    result = foo.method()
    assert result == expected_result

if __name__ == "__main__":
    test()
person simple liquids    schedule 25.05.2015
comment
Хороший подход с насмешкой над контекстом. - person idjaw; 01.03.2016

Вот тот же тест с использованием pytest и модуля:

def test(mocker):
    mock_db = mocker.MagicMock(name='db')
    mocker.patch('db_class.db', new=mock_db)
    expected_result = [5, 10]
    mock_db.a.return_value.__enter__.return_value.b.fetch.return_value = expected_result

    foo = db_class.Foo()
    result = foo.method()
    assert result == expected_result

Вы можете найти то, как я написал тест, более интересным, чем сам тест — я создал библиотеку Python чтобы помочь мне с синтаксисом.

Вот как я подошел к вашей проблеме систематическим образом:

Начнем с нужного вам теста и моей вспомогательной библиотеки:

import db_class

from mock_autogen.pytest_mocker import PytestMocker

def test(mocker):
    # this would output the mocks we need
    print(PytestMocker(db_class).mock_modules().prepare_asserts_calls().generate())

    # your original test, without the mocks
    expected_result = [5,10]
    foo = db_class.Foo()
    result = foo.method()
    assert result == expected_result

Теперь тест явно не пройден (AttributeError: module 'db' has no attribute 'a'), но вывод на печать полезен:

# mocked modules
mock_db = mocker.MagicMock(name='db')
mocker.patch('db_class.db', new=mock_db)
# calls to generate_asserts, put this after the 'act'
import mock_autogen
print(mock_autogen.generator.generate_asserts(mock_db, name='mock_db'))

Теперь я помещаю моки перед вызовом Foo() и generate_asserts после, как раз перед вашим утверждением, вот так (нет необходимости в предыдущей печати, поэтому я удалил ее):

def test(mocker):
    # mocked modules
    mock_db = mocker.MagicMock(name='db')
    mocker.patch('db_class.db', new=mock_db)

    # your original test, without the mocks
    expected_result = [5,10]
    foo = db_class.Foo()
    result = foo.method()

    # calls to generate_asserts, put this after the 'act'
    import mock_autogen
    print(mock_autogen.generator.generate_asserts(mock_db, name='mock_db'))

    assert result == expected_result

Теперь утверждение терпит неудачу (AssertionError: assert <MagicMock name='db.a().__enter__().b.fetch()' id='139996983259768'> == [5, 10]), но мы еще раз получили ценную информацию:

mock_db.a.return_value.__enter__.assert_called_once_with()
mock_db.a.return_value.__enter__.return_value.b.fetch.assert_called_once_with()
mock_db.a.return_value.__exit__.assert_called_once_with(None, None, None)

Обратите внимание на вторую строку, это почти то, что вам нужно издеваться. С небольшим изменением он будет выглядеть как mock_db.a.return_value.__enter__.return_value.b.fetch.return_value = expected_result, и с этим мы можем получить окончательный вариант теста:

def test(mocker):
    mock_db = mocker.MagicMock(name='db')
    mocker.patch('db_class.db', new=mock_db)
    expected_result = [5, 10]
    mock_db.a.return_value.__enter__.return_value.b.fetch.return_value = expected_result

    foo = db_class.Foo()
    result = foo.method()
    assert result == expected_result

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

person Peter K    schedule 06.08.2019