我有一个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()函数开始测试是一种更好的单元测试策略。这样可以更本地地模拟inputsys.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 可用于临时替换模块中的功能(除其他用途外)。在这种情况下,它用于替换inputsys.stdout以允许注入(inject)用户输入并捕获打印的字符串
    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
    

    关于python - Pytest与argparse : how to test user is prompted for confirmation?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/48359957/

    10-12 17:04