带有 argparse 的 Pytest:如何提示用户进行确认?
Pytest with argparse: how to test user is prompted for confirmation?
我有一个 CLI 工具,想测试是否提示用户使用 input()
确认选择。这相当于在 Python 中使用 raw_input()
2.
代码
要测试的(解释的)代码如下所示:
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
这样的东西在本地被更多地模拟。然后,一旦 assured 确认按预期工作,就可以编写测试来验证它是否以特定方式被调用。您可以为此编写测试,并在这些测试期间模拟 confirm()
。
这是 confirm()
的单元测试,它使用 pytest.parametrize
and 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()
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())
结果:
============================= 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()
:
要测试 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
我有一个 CLI 工具,想测试是否提示用户使用 input()
确认选择。这相当于在 Python 中使用 raw_input()
2.
代码
要测试的(解释的)代码如下所示:
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
这样的东西在本地被更多地模拟。然后,一旦 assured 确认按预期工作,就可以编写测试来验证它是否以特定方式被调用。您可以为此编写测试,并在这些测试期间模拟 confirm()
。
这是 confirm()
的单元测试,它使用 pytest.parametrize
and 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()
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())
结果:
============================= 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()
:
要测试 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