如何使用单元测试测试 Python 脚本中的标准输入和标准输出?

How can I test the standard input and standard output in Python Script with a Unittest test?

我正在尝试测试一个 Python 脚本 (2.7),我在其中使用标准输入(使用 raw_input() 读取并使用简单打印编写)但我没有找到如何做到这一点,我相信这个问题很简单。

这是我脚本的非常非常非常的简历代码:

def example():
    number = raw_input()
    print number

if __name__ == '__main__':
    example()

我想写一个 unittest 测试来检查这个,但我不知道如何做。我已经尝试使用 StringIO 和其他东西,但我没有找到真正简单的解决方案。

有人有想法吗?

PD: 当然在真实的脚本中我使用了多行数据块和其他类型的数据。

非常感谢。

编辑:

非常感谢您提供第一个真正具体的答案,它工作得很好,只是我在导入 StringIO 时遇到了一点问题,因为我正在导入 StringIO 并且我需要导入 [=14] =](我真的不明白为什么),但不管怎样,它确实有效。

但是我用这种方式发现了另一个问题,在我的项目中我需要用这种方式测试脚本(感谢您的支持,它工作得很好)但我想这样做: 我有一个包含很多测试的文件来传递脚本,所以我打开文件并读取信息块及其结果块,我想这样做代码将能够处理一个块来检查它们的结果并执行和另一个一样...

像这样:

class Test(unittest.TestCase):
    ...
    #open file and process saving data like datablocks and results
    ...
    allTest = True
    for test in tests:
        stub_stdin(self, test.dataBlock)
        stub_stdouts(self)
        runScrip()
        if sys.stdout.getvalue() != test.expectResult:
            allTest = False

    self.assertEqual(allTest, True)

我知道也许 unittest 现在没有意义,但你可以做一个关于我想要的想法。所以,这种方式失败了,我不知道为什么。

典型的技术包括用您想要的项目模拟标准 sys.stdinsys.stdout。如果你不关心 Python 3 兼容性你可以只使用 StringIO 模块,但是如果你想要前瞻性思维并且愿意限制到 Python 2.7 和 3.3+,支持这个Python 2 和 3 无需通过 io 模块进行太多工作即可成为可能(但需要进行一些修改,但暂时搁置此想法)。

假设你已经有一个 unittest.TestCase 了,你可以创建一个效用函数(或同一个 class 中的方法)来替换 sys.stdin/sys.stdout 作为概述。首先是进口:

import sys
import io
import unittest

在我最近的一个项目中,我为 stdin 做了这个,它需要一个 str 作为输入,用户(或另一个程序通过管道)将作为 stdin 输入你的输入:

def stub_stdin(testcase_inst, inputs):
    stdin = sys.stdin

    def cleanup():
        sys.stdin = stdin

    testcase_inst.addCleanup(cleanup)
    sys.stdin = StringIO(inputs)

至于标准输出和标准错误:

def stub_stdouts(testcase_inst):
    stderr = sys.stderr
    stdout = sys.stdout

    def cleanup():
        sys.stderr = stderr
        sys.stdout = stdout

    testcase_inst.addCleanup(cleanup)
    sys.stderr = StringIO()
    sys.stdout = StringIO()

请注意,在这两种情况下,它都接受一个测试用例实例,并调用其 addCleanup 方法,该方法添加了 cleanup 函数调用,将它们重置回它们在持续时间时的位置试验方法总结。结果是,从在测试用例中调用它到结束的持续时间,sys.stdout 和朋友将被替换为 io.StringIO 版本,这意味着您可以轻松检查其值,并且不要不用担心留下一团糟。

最好举个例子。要使用它,您可以像这样简单地创建一个测试用例:

class ExampleTestCase(unittest.TestCase): 

    def test_example(self):
        stub_stdin(self, '42')
        stub_stdouts(self)
        example()
        self.assertEqual(sys.stdout.getvalue(), '42\n')

现在,在 Python 2 中,只有当 StringIO class 来自 StringIO 模块时,此测试才会通过,而在 Python 3 中不存在这样的模块。您可以做的是使用 io 模块中的版本并进行修改,使其在接受的输入方面稍微宽松一些,这样 unicode encoding/decoding 将自动完成,而不是触发异常(例如 Python 2 中的 print 语句如果没有以下内容将无法正常工作)。我通常这样做是为了 Python 2 和 3 之间的交叉兼容性:

class StringIO(io.StringIO):
    """
    A "safely" wrapped version
    """

    def __init__(self, value=''):
        value = value.encode('utf8', 'backslashreplace').decode('utf8')
        io.StringIO.__init__(self, value)

    def write(self, msg):
        io.StringIO.write(self, msg.encode(
            'utf8', 'backslashreplace').decode('utf8'))

现在将您的示例函数加上此答案中的每个代码片段放入一个文件中,您将获得适用于 Python 2 和 3 的自包含单元测试(尽管您需要调用 print 作为 Python 中的函数 3) 针对 stdio 进行测试。

请注意:如果每个测试方法都需要,您始终可以将 stub_ 函数调用放在 TestCasesetUp 方法中。

当然,如果你想使用各种模拟相关的库来存根stdin/stdout,你可以自由地这样做,但如果这是你的目标,这种方式不依赖于外部依赖。


对于你的第二个问题,测试用例必须以某种方式编写,它们必须封装在一个方法中,而不是在 class 级别,你的原始示例将失败。但是你可能想做这样的事情:

class Test(unittest.TestCase):

    def helper(self, data, answer, runner):
        stub_stdin(self, data)
        stub_stdouts(self)
        runner()
        self.assertEqual(sys.stdout.getvalue(), answer)
        self.doCleanups()  # optional, see comments below

    def test_various_inputs(self):
        data_and_answers = [
            ('hello', 'HELLOhello'),
            ('goodbye', 'GOODBYEgoodbye'),
        ]

        runScript = upperlower  # the function I want to test 

        for data, answer in data_and_answers:
            self.helper(data, answer, runScript)

你可能想要调用 doCleanups 的原因是为了防止清理堆栈变得和所有 data_and_answers 对一样深,但这会将清理堆栈中的所有内容弹出,所以如果你有任何其他需要在最后清理的东西,这可能最终会成为问题 - 你可以自由地将它留在那里,因为所有与 stdio 相关的对象将在最后以相同的顺序恢复,所以真正的一个将永远在那里。现在我要测试的功能:

def upperlower():
    raw = raw_input()
    print (raw.upper() + raw),

所以是的,对我所做的一些解释可能会有所帮助:记住在 TestCase class 中,框架严格依赖于实例的 assertEqual 和它的朋友功能。因此,为了确保在正确的级别进行测试,您确实希望始终调用这些断言,以便在 inputs/answers 出现错误时显示有用的错误消息,但并未完全正确显示,而不是像您对 for 循环所做的那样直到最后(这会告诉您出了点问题,但不完全是数百个错误中的一个,现在您很生气)。还有 helper 方法——你可以随意调用它,只要它不是以 test 开头,因为那样的话框架会尝试 运行 它作为一个,它会失败可怕。所以只要遵循这个约定,你基本上可以在你的测试用例中有模板来 运行 你的测试 - 然后你可以在一个循环中使用它和一堆 inputs/outputs 就像我所做的那样。

关于你的另一个问题:

only I've had a little problem importing StringIO, because I was doing import StringIO and I needed to import like from StringIO import StringIO (I don't understand really why), but be that as It may, it works.

好吧,如果你看一下我的原始代码,我确实向你展示了 import io 是如何实现的,然后通过定义 class StringIO(io.StringIO) 覆盖了 io.StringIO class。然而,它对你有用,因为你是严格从 Python 2 开始做的,而我确实尽可能将我的答案定位到 Python 3,因为 Python 2 会(这次可能是肯定的) ) 在不到 5 年的时间内不会得到支持。想想未来可能正在阅读这篇文章的用户 post,他们遇到了与您类似的问题。无论如何,是的,原来的 from StringIO import StringIO 有效,因为那是 StringIO 模块中的 StringIO class。尽管 from cStringIO import StringIO 应该可以导入 StringIO 模块的 C 版本。它之所以有效,是因为它们都提供了足够接近的接口,因此它们基本上会按预期工作(当然,直到您尝试 运行 在 Python 3 下这样做)。

同样,将所有这些与我的代码放在一起应该会产生 self-contained working test script。请记住查看文档并遵循代码的形式,而不是发明自己的语法并希望事情能起作用(至于为什么你的代码不起作用,因为 "test" 代码定义在class 正在构建,因此所有这些都是在 Python 导入模块时执行的,并且由于测试 运行 所需的 none 是即使可用(即 class 本身甚至还不存在),整个事情只是在抽搐的痛苦中死去。在这里提问也有帮助,即使您遇到的问题确实很常见,但没有一个快速简单的名称来搜索您的确切问题确实很难找出您哪里出了问题,我想是吧? :) 无论如何祝你好运,感谢你努力测试你的代码。


还有其他方法,但考虑到其他 questions/answers 我在这里查看 SO 似乎没有帮助,我希望这个。其他供参考:

  • How to supply stdin, files and environment variable inputs to Python unit tests?
  • python mocking raw input in unittests

当然,无需重复,所有这些 都可以 使用 unittest.mock available in Python 3.3+ or the original/rolling backport version on pypi, but given that those libraries hides some of the intricacies on what actually happens, they may end up hiding some of the details on what actually happens (or need to happen) or how the redirection actually happens. If you want, you can read up on unittest.mock.patch 完成,然后稍微下降到 StringIO 补丁 sys.stdout部分。