Как проверить функцию с входным вызовом?

У меня есть консольная программа, написанная на Python. Он задает пользователю вопросы с помощью команды:

some_input = input('Answer the question:', ...)

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

Ответы

Ответ 1

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

import module  # The module which contains the call to input

class TestClass:

    def test_function_1(self):
        # Override the Python built-in input method 
        module.input = lambda: 'some_input'
        # Call the function you would like to test (which uses input)
        output = module.function()  
        assert output == 'expected_output'

    def test_function_2(self):
        module.input = lambda: 'some_other_input'
        output = module.function()  
        assert output == 'another_expected_output'        

    def teardown_method(self, method):
        # This method is being called after each test case, and it will revert input back to original function
        module.input = input  

Более элегантным решением было бы использовать модуль mock вместе с оператором with statement. Таким образом, вам не нужно использовать teardown, а пропатченный метод будет жить только в области with.

import mock
import module

def test_function():
    with mock.patch.object(__builtins__, 'input', lambda: 'some_input'):
        assert module.function() == 'expected_output'

Ответ 2

Как предположил Компилятор, в pytest есть новое приспособление monkeypatch для этого. Объект monkeypatch может изменить атрибут в классе или значение в словаре, а затем восстановить его исходное значение в конце теста.

В этом случае встроенная функция input является значением словаря python __builtins__, поэтому мы можем изменить его следующим образом:

def test_something_that_involves_user_input(monkeypatch):

    # monkeypatch the "input" function, so that it returns "Mark".
    # This simulates the user entering "Mark" in the terminal:
    monkeypatch.setattr('builtins.input', lambda: "Mark")

    # go about using input() like you normally would:
    i = input("What is your name?")
    assert i == "Mark"

Ответ 3

Вы можете заменить sys.stdin на некоторый пользовательский текстовый ввод-вывод, такой как ввод из файла или буфер StringIO в памяти:

import sys

class Test:
    def test_function(self):
        sys.stdin = open("preprogrammed_inputs.txt")
        module.call_function()

    def setup_method(self):
        self.orig_stdin = sys.stdin

    def teardown_method(self):
        sys.stdin = self.orig_stdin

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

Это также может быть сделано довольно элегантно с помощью собственного менеджера контекста

import sys
from contextlib import contextmanager

@contextmanager
def replace_stdin(target):
    orig = sys.stdin
    sys.stdin = target
    yield
    sys.stdin = orig

А затем просто используйте это, например, так:

with replace_stdin(StringIO("some preprogrammed input")):
    module.call_function()

Ответ 4

Вы можете сделать это с помощью mock.patch следующим образом.

Сначала в вашем коде создайте фиктивную функцию для вызовов input:

def __get_input(text):
    return input(text)

В ваших тестовых функциях:

import my_module
from mock import patch

@patch('my_module.__get_input', return_value='y')
def test_what_happens_when_answering_yes(self, mock):
    """
    Test what happens when user input is 'y'
    """
    # whatever your test function does

Например, если у вас есть цикл, проверяющий, что единственные допустимые ответы находятся в ['y', 'Y', 'n', 'N'], вы можете проверить, что ничего не происходит при вводе другого значения.

В этом случае мы предполагаем, что SystemExit вызывается при ответе "N":

@patch('my_module.__get_input')
def test_invalid_answer_remains_in_loop(self, mock):
    """
    Test nothing broken when answer is not ['Y', 'y', 'N', 'n']
    """
    with self.assertRaises(SystemExit):
        mock.side_effect = ['k', 'l', 'yeah', 'N']
        # call to our function asking for input

Ответ 5

Это может быть сделано с mock.patch и with блоками в Python3.

import pytest
import mock
import builtins

"""
The function to test (would usually be loaded
from a module outside this file).
"""
def user_prompt():
    ans = input('Enter a number: ')
    try:
        float(ans)
    except:
        import sys
        sys.exit('NaN')
    return 'Your number is {}'.format(ans)

"""
This test will mock input of '19'
"""    
def test_user_prompt_ok():
    with mock.patch.object(builtins, 'input', lambda _: '19'):
        assert user_prompt() == 'Your number is 19'

Обратите внимание на mock.patch.object(builtins, 'input', lambda _: '19'): которая переопределяет input с помощью функции lambda. Наша лямбда-функция принимает одноразовую переменную _ потому что input принимает аргумент.

Вот как вы можете проверить случай сбоя, где user_input вызывает sys.exit. Хитрость здесь в том, чтобы заставить pytest искать это исключение с помощью pytest.raises(SystemExit).

"""
This test will mock input of 'nineteen'
"""    
def test_user_prompt_exit():
    with mock.patch.object(builtins, 'input', lambda _: 'nineteen'):
        with pytest.raises(SystemExit):
            user_prompt()

Вы сможете запустить этот тест, скопировав и вставив приведенный выше код в файл tests/test_.py и запустив pytest из родительского pytest.