Лучшие практики обезьяньего исправления Python

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

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

Например, предположим, что у меня есть функция GET из внешней библиотеки, моя test_a() нуждается в исправлении GET(), чтобы она возвращала False, а test_b() нуждается в исправлении GET(), чтобы она возвращала True.

Каков предпочтительный способ обработки такого сценария. В настоящее время я делаю следующее:

def test_a(monkeypatch):
    my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)

def test_b(monkeypatch)
    my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)

def test_c(monkeypatch)
    my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = True)

def my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = False):

    def patch_func_pos():
        return True

    patch_func_neg():
        return False

    patch_func_exception():
        raise my_exception

    if patch_get_to_return_true:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_pos)

    if patch_get_to_return_false:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_neg)

    if patch_get_to_raise_exception:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_exception)

В приведенном выше примере всего три теста, которые исправляют одну функцию. Мой фактический тестовый файл содержит около 20 тестов, и каждый тест будет дополнительно исправлять несколько функций.

Может ли кто-нибудь предложить мне лучший способ справиться с этим? Рекомендуется ли вынести часть monkeypatching в отдельный файл?


person sjaymj62    schedule 18.07.2018    source источник


Ответы (1)


Не зная подробностей, я бы предложил разбить my_patcher на несколько небольших приборов:

@pytest.fixture
def mocked_GET_pos(monkeypatch):
    monkeypatch.setattr(ExternalLib, 'GET', lambda: True)


@pytest.fixture
def mocked_GET_neg(monkeypatch):
    monkeypatch.setattr(ExternalLib, 'GET', lambda: False)


@pytest.fixture
def mocked_GET_raises(monkeypatch):
    def raise_():
        raise Exception()
    monkeypatch.setattr(ExternalLib, 'GET', raise_)

Теперь используйте pytest.mark.usefixtures для автоматического применения прибора в тесте:

@pytest.mark.usefixtures('mocked_GET_pos')
def test_GET_pos():
    assert ExternalLib.GET()


@pytest.mark.usefixtures('mocked_GET_neg')
def test_GET_neg():
    assert not ExternalLib.GET()


@pytest.mark.usefixtures('mocked_GET_raises')
def test_GET_raises():
    with pytest.raises(Exception):
        ExternalLib.GET()

Тем не менее, есть место для улучшений, в зависимости от фактического контекста. Например, когда логика тестов одна и та же, а единственное, что различается, — это некоторые предварительные условия теста (например, разные исправления GET в вашем случае), параметризация тестов или фикстур часто позволяет сэкономить много дублирования кода. Представьте, что у вас есть собственная функция, которая вызывает GET внутри:

# my_lib.py

def inform():
    try:
        result = ExternalLib.GET()
    except Exception:
        return 'error'
    if result:
        return 'success'
    else:
        return 'failure'

и вы хотите проверить, возвращает ли он действительный результат независимо от поведения GET:

# test_my_lib.py

def test_inform():
    assert inform() in ['success', 'failure', 'error']

Используя описанный выше подход, вам нужно будет скопировать test_inform три раза, единственная разница между копиями заключается в том, что используется другой прибор. Этого можно избежать, написав параметризованную фикстуру, которая будет принимать несколько возможностей исправления для GET:

@pytest.fixture(params=[lambda: True,
                        lambda: False,
                        raise_],
                ids=['pos', 'neg', 'exception'])
def mocked_GET(request):
    monkeypatch.setattr(ExternalLib, 'GET', request.param)

Теперь при применении mocked_GET к test_inform:

@pytest.mark.usefixtures('mocked_GET')
def test_inform():
    assert inform() in ['success', 'failure', 'error']

вы получаете три теста из одного: test_inform будет запускаться три раза, по одному разу с каждым макетом, переданным в mocked_GET параметры.

test_inform[pos]
test_inform[neg]
test_inform[exception]

Тесты тоже можно параметризовать (через pytest.mark.parametrize), и при правильном применении метод параметризации экономит много шаблонного кода.

person hoefling    schedule 18.07.2018
comment
Спасибо за ваш подробный ответ. Я знал об обезьяньих исправлениях, фикстурах и параметризованных тестах. Но я никогда не думал об объединении всех этих концепций, чтобы избежать дублирования кода. Ваш ответ действительно помог мне. Жаль, что я могу дать вам только один голос. - person sjaymj62; 19.07.2018
comment
Рад, что смог помочь! Когда я впервые начал работать с pytest, мне очень помогло чтение официальной документации, там много отличных примеров. - person hoefling; 19.07.2018