Pytest与argparse:如何测试用户提示确认? [英] Pytest with argparse: how to test user is prompted for confirmation?
问题描述
我有一个CLI工具,并想测试是否提示用户使用input()
确认选择.这等效于在Python 2中使用raw_input()
.
代码
要测试的(释义的)代码如下:
import sys
import argparse
def confirm():
notification_str = "Please respond with 'y' or 'n'"
while True:
choice = input("Confirm [Y/n]?").lower()
if choice in 'yes' or not choice:
return True
if choice in 'no':
return False
print(notification_str)
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--destructive', action='store_true')
return parser.parse_args()
def main():
args = parse_args(sys.argv[1:])
if args.destructive:
if not confirm():
sys.exit()
do_stuff(args)
if __name__ == '__main__':
main()
问题
我正在使用pytest作为我的框架.我该如何做才能测试CLI中是否显示确认提示?如果我尝试比较stdout
,则会收到错误消息:OSError: reading from stdin while output is captured
.
我要确保:
- 设置破坏性标志时显示确认
- 不显示时不显示
我将在另一个文件中使用以下代码:
import pytest
from module_name import main
def test_user_is_prompted_when_destructive_flag_is_set():
sys.argv['', '-d']
main()
assert _ # What the hell goes here?
def test_user_is_not_prompted_when_destructive_flag_not_set():
sys.argv['',]
main()
assert _ # And here too?
我建议使用confirm()
函数开始测试是一种更好的单元测试策略.这样可以更本地地模拟input
和sys.stdio
之类的东西.然后,一旦确定可以按预期工作,就可以编写测试以验证它是否以特定方式被调用.您可以为此编写测试,并在那些测试中模拟confirm()
.
这是confirm()
的单元测试,它使用 pytest.parametrize
和 mock
处理用户输入和输出:
代码:
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
def test_get_from_user(from_user, response, output):
from_user = list(from_user) if isinstance(from_user, list) else [from_user]
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
assert response == confirm()
assert output == sys.stdout.getvalue()
这是如何工作的?
pytest.mark.parametrize
允许根据条件轻松多次调用测试功能.这是4个简单的步骤,它们将测试confirm
中的大多数功能:
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
可以使用
mock.patch
临时替换模块中的功能(除其他用途外).在这种情况下,它用于替换input
和sys.stdout
以允许注入用户输入并捕获打印的字符串
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
最后运行被测函数,并验证该函数的输出和所有打印的字符串:
assert response == confirm()
assert output == sys.stdout.getvalue()
测试代码(用于测试代码):
import sys
from io import StringIO
import pytest
from unittest import mock
import builtins
def confirm():
notification_str = "Please respond with 'y' or 'n'"
while True:
choice = input("Confirm [Y/n]?").lower()
if choice in 'yes' or not choice:
return True
if choice in 'no':
return False
print(notification_str)
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
def test_get_from_user(from_user, response, output):
from_user = list(from_user) if isinstance(from_user, list) \
else [from_user]
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
assert response == confirm()
assert output == sys.stdout.getvalue()
pytest.main('-x test.py'.split())
结果:
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: C:\Users\stephen\Documents\src\testcode, inifile:
collected 4 items
test.py .... [100%]
========================== 4 passed in 0.15 seconds ===========================
对confirm()
的测试调用:
要测试确认是否在预期的时间被调用以及程序在预期的时间响应,可以使用unittest.mock
来模拟confirm()
.
注意:在通常的单元测试方案中,confirm
将位于不同的文件中,并且mock.patch
可以与本示例中修补sys.argv
的方式类似的方式使用.
用于检查对confirm()
的调用的测试代码:
import sys
import argparse
def confirm():
pass
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--destructive', action='store_true')
return parser.parse_args()
def main():
args = parse_args(sys.argv[1:])
if args.destructive:
if not confirm():
sys.exit()
import pytest
from unittest import mock
@pytest.mark.parametrize("argv, called, response", [
([], False, None),
(['-d'], True, False),
(['-d'], True, True),
])
def test_get_from_user(argv, called, response):
global confirm
original_confirm = confirm
confirm = mock.Mock(return_value=response)
with mock.patch('sys.argv', [''] + argv):
if called and not response:
with pytest.raises(SystemExit):
main()
else:
main()
assert confirm.called == called
confirm = original_confirm
pytest.main('-x test.py'.split())
结果:
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: C:\Users\stephen\Documents\src\testcode, inifile:
collected 3 items
test.py ... [100%]
========================== 3 passed in 3.26 seconds ===========================
enter code here
I have a CLI tool, and would like to test that the user is prompted to confirm a choice using input()
. This would be equivalent as using raw_input()
in Python 2.
Code
The (paraphrased) code to test looks like:
import sys
import argparse
def confirm():
notification_str = "Please respond with 'y' or 'n'"
while True:
choice = input("Confirm [Y/n]?").lower()
if choice in 'yes' or not choice:
return True
if choice in 'no':
return False
print(notification_str)
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--destructive', action='store_true')
return parser.parse_args()
def main():
args = parse_args(sys.argv[1:])
if args.destructive:
if not confirm():
sys.exit()
do_stuff(args)
if __name__ == '__main__':
main()
Question
I am using pytest as my framework. How do I make it so I can test that the confirmation prompt is showing up in the CLI? If I try to compare stdout
I get the error: OSError: reading from stdin while output is captured
.
I want to make sure that:
- The confirmation shows up when the destructive flag is set
- It doesn't show up when it isn't
I will be using the following code in another file:
import pytest
from module_name import main
def test_user_is_prompted_when_destructive_flag_is_set():
sys.argv['', '-d']
main()
assert _ # What the hell goes here?
def test_user_is_not_prompted_when_destructive_flag_not_set():
sys.argv['',]
main()
assert _ # And here too?
I would suggest that starting testing with the confirm()
function is a better unit test strategy. This allows things like input
and sys.stdio
to be mocked more locally. Then once assured confirms works as expected, tests can be written that verify that it is called in specific ways. You can write tests for that, and mock confirm()
during those tests.
Here is a unit test forconfirm()
that uses pytest.parametrize
and mock
to deal with user input and output:
Code:
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
def test_get_from_user(from_user, response, output):
from_user = list(from_user) if isinstance(from_user, list) else [from_user]
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
assert response == confirm()
assert output == sys.stdout.getvalue()
How does this work?
pytest.mark.parametrize
allows a test function to be easily called multple times with conditions. Here are 4 simple steps which will test most of the functionality in confirm
:
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
mock.patch
can be used to temporarily replace a function in module (among other uses). In this case it is used to replace input
and sys.stdout
to allow inject user input, and capture printed strings
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
finally the function under test is run and the output of the function and any string printed are verified:
assert response == confirm()
assert output == sys.stdout.getvalue()
Test Code (for the test code):
import sys
from io import StringIO
import pytest
from unittest import mock
import builtins
def confirm():
notification_str = "Please respond with 'y' or 'n'"
while True:
choice = input("Confirm [Y/n]?").lower()
if choice in 'yes' or not choice:
return True
if choice in 'no':
return False
print(notification_str)
@pytest.mark.parametrize("from_user, response, output", [
(['x', 'x', 'No'], False, "Please respond with 'y' or 'n'\n" * 2),
('y', True, ''),
('n', False, ''),
(['x', 'y'], True, "Please respond with 'y' or 'n'\n"),
])
def test_get_from_user(from_user, response, output):
from_user = list(from_user) if isinstance(from_user, list) \
else [from_user]
with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):
with mock.patch('sys.stdout', new_callable=StringIO):
assert response == confirm()
assert output == sys.stdout.getvalue()
pytest.main('-x test.py'.split())
Results:
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: C:\Users\stephen\Documents\src\testcode, inifile:
collected 4 items
test.py .... [100%]
========================== 4 passed in 0.15 seconds ===========================
Test Calls to confirm()
:
To test that confirm is called when expected, and that the program responds as expected when called, you can use unittest.mock
to mock confirm()
.
Note: In the usual unittest scenario, confirm
would be in a different file and mock.patch
could be used in a similiar manner to how sys.argv
is patched in this example.
Test Code for checking calls to confirm()
:
import sys
import argparse
def confirm():
pass
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--destructive', action='store_true')
return parser.parse_args()
def main():
args = parse_args(sys.argv[1:])
if args.destructive:
if not confirm():
sys.exit()
import pytest
from unittest import mock
@pytest.mark.parametrize("argv, called, response", [
([], False, None),
(['-d'], True, False),
(['-d'], True, True),
])
def test_get_from_user(argv, called, response):
global confirm
original_confirm = confirm
confirm = mock.Mock(return_value=response)
with mock.patch('sys.argv', [''] + argv):
if called and not response:
with pytest.raises(SystemExit):
main()
else:
main()
assert confirm.called == called
confirm = original_confirm
pytest.main('-x test.py'.split())
Results:
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: C:\Users\stephen\Documents\src\testcode, inifile:
collected 3 items
test.py ... [100%]
========================== 3 passed in 3.26 seconds ===========================
enter code here
这篇关于Pytest与argparse:如何测试用户提示确认?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!