Как проверить функцию с входным вызовом?
У меня есть консольная программа, написанная на 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
.