带有 argparse 的 Pytest:提示如何测试用户进行确认? [英] Pytest with argparse: how to test user is prompted for confirmation?

查看:25
本文介绍了带有 argparse 的 Pytest:提示如何测试用户进行确认?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个 CLI 工具,想测试是否提示用户使用 input() 确认选择.这相当于在 Python 2 中使用 raw_input().

代码

要测试的(转述)代码如下所示:

导入系统导入参数解析定义确认():notification_str = "请回复 'y' 或 'n'"而真:selection = input("Confirm [Y/n]?").lower()如果选择是"或不选择:返回真如果选择否":返回错误打印(notification_str)def parse_args(args):解析器 = argparse.ArgumentParser()parser.add_argument('-d', '--破坏性', action='store_true')返回 parser.parse_args()定义主():args = parse_args(sys.argv[1:])如果 args.破坏性:如果没有确认():系统退出()do_stuff(参数)如果 __name__ == '__main__':主要的()

问题

我使用 pytest 作为我的框架.我如何才能测试确认提示是否显示在 CLI 中?如果我尝试比较 stdout,我会收到错误:OSError:在捕获输出时从 stdin 读取.

我想确保:

  1. 设置破坏性标志时会显示确认
  2. 不显示时不显示

我将在另一个文件中使用以下代码:

导入pytestfrom module_name import maindef test_user_is_prompted_when_corruption_flag_is_set():sys.argv['', '-d']主要的()assert _ # 这到底是怎么回事?def test_user_is_not_prompted_when_corruption_flag_not_set():sys.argv['',]主要的()assert _ # 这里也是?

解决方案

我建议使用 confirm() 函数开始测试是更好的单元测试策略.这允许像 inputsys.stdio 这样的东西在本地被更多地模拟.然后,一旦保证按预期工作,就可以编写测试来验证它是否以特定方式调用.您可以为此编写测试,并在这些测试期间模拟 confirm().

这是confirm() 的单元测试,它使用 pytest.parametrizemock 处理用户输入输出:

代码:

@pytest.mark.parametrize("from_user, response, output", [(['x', 'x', 'No'], False, "请回复 'y' 或 'n'\n" * 2),('y', 真, ''),('n', 假, ''),(['x', 'y'], True, "请回复 'y' 或 'n'\n"),])def test_get_from_user(from_user, response, output):from_user = list(from_user) if isinstance(from_user, list) else [from_user]使用 mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):使用 mock.patch('sys.stdout', new_callable=StringIO):断言响应==确认()断言输出 == sys.stdout.getvalue()

这是如何工作的?

pytest.mark.parametrize 允许根据条件轻松地多次调用测试函数.这里有 4 个简单的步骤,它们将测试 confirm 中的大部分功能:

@pytest.mark.parametrize("from_user, response, output", [(['x', 'x', 'No'], False, "请回复 'y' 或 'n'\n" * 2),('y', 真, ''),('n', 假, ''),(['x', 'y'], True, "请回复 'y' 或 'n'\n"),])

mock.patch 可用于临时替换模块中的函数(以及其他用途).在这种情况下,它用于替换 inputsys.stdout 以允许注入用户输入,并捕获打印的字符串

 with mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):使用 mock.patch('sys.stdout', new_callable=StringIO):

最后运行被测函数并验证函数的输出和打印的任何字符串:

assert response == confirm()断言输出 == sys.stdout.getvalue()

测试代码(用于测试代码):

导入系统从 io 导入 StringIO导入pytest从单元测试导入模拟导入内置函数定义确认():notification_str = "请回复 'y' 或 'n'"而真:selection = input("Confirm [Y/n]?").lower()如果选择是"或不选择:返回真如果选择否":返回错误打印(notification_str)@pytest.mark.parametrize("from_user, response, output", [(['x', 'x', 'No'], False, "请回复 'y' 或 'n'\n" * 2),('y', 真, ''),('n', 假, ''),(['x', 'y'], True, "请回复 'y' 或 'n'\n"),])def test_get_from_user(from_user, response, output):from_user = list(from_user) if isinstance(from_user, list) \否则 [来自_用户]使用 mock.patch.object(builtins, 'input', lambda x: from_user.pop(0)):使用 mock.patch('sys.stdout', new_callable=StringIO):断言响应==确认()断言输出 == sys.stdout.getvalue()pytest.main('-x test.py'.split())

结果:

============================== 测试会话开始 ==============================平台 win32 -- Python 3.6.3、pytest-3.3.2、py-1.5.2、pluggy-0.6.0rootdir: C:\Users\stephen\Documents\src\testcode, inifile:收集 4 项test.py .... [100%]========================== 4 在 0.15 秒内通过 ==========================

confirm() 的测试调用:

要测试在预期时调用确认,并且程序在调用时按预期响应,您可以使用 unittest.mock 来模拟 confirm().

注意:在通常的单元测试场景中,confirm 将位于不同的文件中,并且 mock.patch 可以以与 sys.path 类似的方式使用.argv 在本例中被修补.

用于检查对 confirm() 调用的测试代码:

导入系统导入参数解析定义确认():经过def parse_args(args):解析器 = argparse.ArgumentParser()parser.add_argument('-d', '--破坏性', action='store_true')返回 parser.parse_args()定义主():args = parse_args(sys.argv[1:])如果 args.破坏性:如果没有确认():系统退出()导入pytest从单元测试导入模拟@pytest.mark.parametrize("argv, 调用,响应", [([], 假, 无),(['-d'], 真, 假),(['-d'], 真, 真),])def test_get_from_user(argv, call, response):全球确认original_confirm = 确认确认 = mock.Mock(return_value=response)使用 mock.patch('sys.argv', [''] + argv):如果被调用但没有响应:使用 pytest.raises(SystemExit):主要的()别的:主要的()断言确认.调用 == 调用确认 = original_confirmpytest.main('-x test.py'.split())

结果:

============================== 测试会话开始 ==============================平台 win32 -- Python 3.6.3、pytest-3.3.2、py-1.5.2、pluggy-0.6.0rootdir: C:\Users\stephen\Documents\src\testcode, inifile:收集了 3 件物品test.py ... [100%]========================== 3 在 3.26 秒内通过 ==========================在此处输入代码

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:

  1. The confirmation shows up when the destructive flag is set
  2. 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

这篇关于带有 argparse 的 Pytest:提示如何测试用户进行确认?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆