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

2022-05-17 00:00:00 python python-unittest io

问题描述

我正在尝试测试一个使用Standar输入(用RAW_INPUT()读取,用一个简单的打印编写)的Python脚本(2.7),但我不知道如何做到这一点,而且我确信这个问题非常简单。

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

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

if __name__ == '__main__':
    example()

我想编写一个单元测试来检查这一点,但我不知道如何进行。我尝试过StringIO和其他东西,但我找不到真正简单的解决方案。

有人有主意吗?

pd:当然,在真正的脚本中,我使用的是带有几行的数据块和其他类型的数据。

非常感谢。

编辑:

非常感谢第一个非常具体的答案,它工作得很好,只是我在导入StringIO时遇到了一点问题,因为我正在执行导入StringIO,并且我需要像from StringIO import StringIO一样导入(我不知道真正的原因),但尽管如此,它仍然可以工作。

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

类似以下内容:

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)

我知道单元测试现在可能没有意义,但你可以做一个关于我想要的想法。所以,这种方法失败了,我不知道为什么。


解决方案

典型技术包括使用您想要的项目模拟标准sys.stdinsys.stdout。如果您不关心与Python3的兼容性,您可以只使用StringIO模块,但是,如果您想要向前看,并愿意将其限制到Python2.7和3.3+,则可以通过这种方式同时支持Python2和3,而不需要通过io模块进行太多工作(但需要进行一些修改,但暂时搁置这一想法)。

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

import sys
import io
import unittest

在我最近的一个项目中,我为stdin做了这件事,用户(或通过管道的另一个程序)将作为stdin:

输入到您的程序中的输入需要str
def stub_stdin(testcase_inst, inputs):
    stdin = sys.stdin

    def cleanup():
        sys.stdin = stdin

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

至于stdout和stderr:

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和Friends将被io.StringIO版本替换,这意味着您可以轻松地检查它的值,并且不必担心留下一团糟。

最好以此为例进行说明。要使用它,您只需创建一个测试用例,如下所示:

class ExampleTestCase(unittest.TestCase): 

    def test_example(self):
        stub_stdin(self, '42')
        stub_stdouts(self)
        example()
        self.assertEqual(sys.stdout.getvalue(), '42
')
现在,在Python2中,只有当StringIO类来自StringIO模块时,该测试才会通过,而在Python3中不存在这样的模块。您可以做的是使用io模块中的版本,并对其进行修改,使其在接受输入方面稍微宽松一些,以便Unicode编码/解码将自动完成,而不是触发异常(例如,如果没有以下内容,Python2中的print语句将不能很好地工作)。我通常这样做是为了在Python2和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'))

现在将您的示例函数和此答案中的每个代码片段插入到一个文件中,您将获得在Python2和3中都可以工作的自包含单元测试(尽管您需要在Python3中作为函数调用print),以便对Stdio进行测试。

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

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


对于您的第二个问题,测试用例必须以某种方式编写,测试用例必须封装在方法中,而不是在类级别,您的原始示例将失败。但是,您可能希望执行以下操作:

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类中,框架严格依赖于实例的assertEqual和朋友才能运行。因此,为了确保在正确的级别上进行测试,您确实希望始终调用这些断言,以便在错误发生时显示有用的错误消息,而不是像您使用for循环所做的那样,直到最后才显示错误消息,而不是像您使用for循环所做的那样(这将告诉您出了什么问题,但不是数百个错误中的确切位置,现在您疯了)。还有helper方法--你可以叫它任何你想要的名字,只要它不是以test开头就行了,因为这样框架就会试着把它作为一个来运行,它就会失败得很厉害。因此,只要遵循这个约定,您基本上就可以在您的测试用例中使用模板来运行您的测试--然后您可以像我所做的那样,在带有大量输入/输出的循环中使用它。

关于你的另一个问题:

只是我在导入StringIO时遇到了一点小问题,因为我正在导入StringIO,并且我需要像从StringIO导入StringIO一样导入StringIO(我不知道真正的原因),但尽管如此,它仍然可以工作。

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

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


还有其他方法,但考虑到我在这里看到的其他问题/答案似乎没有帮助,我希望这个方法。其他参考:

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

很自然地,它会重复可以使用unittest.mock在Python3.3+或original/rolling backport version on pypi中提供的来完成所有这些工作,但考虑到这些库隐藏了实际发生的一些复杂情况,它们最终可能会隐藏有关实际发生(或需要发生什么)或重定向实际如何发生的一些细节。如果您愿意,可以阅读unittest.mock.patch,然后稍微向下阅读StringIO修补sys.stdout部分。

相关文章