如何使用输入调用测试函数?

How to test a function with input call?

我有一个用 Python 编写的控制台程序。它使用以下命令询问用户问题:

some_input = input('Answer the question:', ...)

我如何测试包含使用 pytest 调用 input 的函数? 我不想强迫测试人员多次输入文本来完成一个测试 运行.

您可能应该模拟内置 input function, you can use the teardown functionality provided by 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 module together with a with statement。这样你就不需要使用拆解,修补后的方法只会存在于 with 范围内。

import mock
import module

def test_function():
    with mock.patch.object(__builtins__, 'input', lambda: 'some_input'):
        assert module.function() == 'expected_output'

正如编译器所建议的那样,pytest 对此有一个新的 monkeypatch 固定装置。 monkeypatch 对象可以更改 class 中的属性或字典中的值,然后在测试结束时恢复其原始值。

在这种情况下,内置的 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"

您可以替换 sys.stdin with some custom Text IO,例如来自文件或内存中 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()

您可以使用 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'] 中,您可以测试什么都没有输入不同的值时会发生这种情况。

In this case we assume a SystemExit is raised when answering 'N':

@patch('my_module.__get_input')
def test_invalid_answer_remains_in_loop(self, mock):
    """
    Test nothing's 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

这可以通过 python3 中的 mock.patchwith 块来完成。

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'):,它用 lambda 函数覆盖了 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 来获得此测试 运行。

因为我需要调用 input() 来暂停和检查我的硬件状态 LED,所以我不得不在没有模拟的情况下处理这种情况。我使用了 -s 标志。

python -m pytest -s test_LEDs.py

-s 标志本质上意味着:--capture=no.

的快捷方式

您还可以在测试代码中使用环境变量。例如,如果您想将路径作为参数,您可以读取环境变量并在缺少时设置默认值。

import os
...
input = os.getenv('INPUT', default='inputDefault/')

然后从默认参数开始

pytest ./mytest.py

或使用自定义参数

INPUT=newInput/ pytest ./mytest.py

另一种不需要使用 lambda 函数并在测试期间提供更多控制的替代方法是使用标准 unittest 模块中的 mock 装饰器。

它还有一个额外的好处,就是只在查找对象(即 input)的地方打补丁,即 the recommended strategy

# path/to/test/module.py
def my_func():
    some_input = input('Answer the question:')
    return some_input
# tests/my_tests.py

from unittest import mock

from path.to.test.module import my_func

@mock.patch("path.to.test.module.input")
def test_something_that_involves_user_input(mock_input):
    mock_input.return_value = "This is my answer!"
    assert my_func() == "This is my answer!"
    mock_input.assert_called_once()  # Optionally check one and only one call