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

Jua*_*nio 6 python io python-unittest

我正在尝试测试标准输入(使用raw_input()读取并以简单的打印方式编写)的Python脚本(2.7),但我找不到该怎么做,并且我确定这个问题是很简单的。

这是我脚本的非常非常恢复的代码:

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

if __name__ == '__main__':
    example()
Run Code Online (Sandbox Code Playgroud)

我想编写一个单元测试来检查这一点,但是我找不到如何做。我正在尝试使用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)
Run Code Online (Sandbox Code Playgroud)

我知道也许单元测试现在没有意义,但是您可以对我想要的做个想法。因此,这种方式失败了,我也不知道为什么。

met*_*ter 5

典型的技术包括模拟标准sys.stdinsys.stdout所需项目。如果您不关心Python 3的兼容性,则可以使用该StringIO模块,但是,如果您要向前思考并愿意将其限制在Python 2.7和3.3+中,则可以通过这种方式同时支持Python 2和3在io模块中工作(但需要进行一些修改,但暂时不要考虑)。

假设您已经准备好了unittest.TestCase,可以创建一个实用程序函数(或同一类中的方法),该函数将替换sys.stdin/,sys.stdout如下所示。首先进口:

import sys
import io
import unittest
Run Code Online (Sandbox Code Playgroud)

在我最近的项目之一中,我已经为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)
Run Code Online (Sandbox Code Playgroud)

至于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()
Run Code Online (Sandbox Code Playgroud)

请注意,在两种情况下,它都接受一个测试用例实例,并调用其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')
Run Code Online (Sandbox Code Playgroud)

现在,在Python 2中,仅当StringIO该类来自StringIO模块时,该测试才能通过,而在Python 3中,则不存在此类模块。您可以做的是使用io模块中的版本,并对其进行修改,使其在接受的输入方面稍微宽松一些,以便Unicode编码/解码将自动完成,而不是触发异常(例如printPython中的语句)如果没有以下内容,则2将无法正常工作)。我通常这样做是为了实现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'))
Run Code Online (Sandbox Code Playgroud)

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

还有一点要注意:如果每个测试方法都需要,则可以始终将stub_函数调用放在setUp方法中TestCase

当然,如果要使用各种与模拟相关的库来存根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)
Run Code Online (Sandbox Code Playgroud)

您可能要调用的原因doCleanups是为了防止清理堆栈变得像所有data_and_answers对一样深,但这会从清理堆栈中弹出所有内容,因此如果最后还有其他需要清理的内容这可能最终会带来问题-您可以随意离开那里,因为所有与stdio相关的对象都将以相同顺序还原到最后,因此真正的对象将始终在那里。现在我要测试的功能:

def upperlower():
    raw = raw_input()
    print (raw.upper() + raw),
Run Code Online (Sandbox Code Playgroud)

因此,是的,我所做的一些解释可能会有所帮助:请记住,在一个TestCase类中,该框架严格依赖于实例assertEqual和其朋友来使其起作用。因此,为了确保在正确的级别上进行测试,您真的想一直调用这些断言,以便在发生错误时(输入/答案显示得不正确)显示有用的错误消息,而不是直到最后,就像您对for循环所做的一样(这将告诉您出了点问题,但不完全是数百个错误中的哪个,现在您很生气)。也是helper方法-您可以随意调用它,只要它不以test因为该框架将尝试将其作为一个整体运行,并且将严重失败。因此,只要遵循此约定,您就可以在测试用例中基本包含模板来运行测试-然后可以像我一样在带有大量输入/输出的循环中使用它。

至于您的其他问题:

只是我在导入StringIO时遇到了一些问题,因为我正在执行导入StringIO,并且需要从StringIO导入,就像从StringIO导入StringIO(我不太明白为什么),但事实上,它确实可行。

好吧,如果您看一下我的原始代码,我确实向您展示了如何做import io,然后io.StringIO通过定义覆盖了该类class StringIO(io.StringIO)。但是它对您有用,因为您严格地从Python 2进行此操作,而鉴于不到5年将不支持Python 2,因此我会尽一切可能将针对Python 3的答案作为目标。想想将来可能正在阅读这篇文章的与您有类似问题的用户。无论如何,是的,原始的from StringIO import StringIO作品,因为那StringIOStringIO模块中的类。虽然from cStringIO import StringIO应该可以导入该文件的C版本StringIO模块。之所以起作用,是因为它们都提供了足够接近的接口,因此它们基本上将按预期工作(直到您尝试在Python 3下运行它)。

同样,将所有这些与我的代码放在一起,应该会产生一个自包含的工作测试脚本。请记住要阅读文档并遵循代码的形式,而不要发明自己的语法并希望事情能够起作用(以及确切地说,为什么您的代码不起作用,因为“测试”代码是在类所在的位置定义的)正在构建中,因此所有这些操作都是在Python导入模块时执行的,并且由于测试运行所需的所有东西甚至都不可用(即类本身甚至还不存在),因此只是因抽搐而死)。我想,即使您面临的问题确实很普遍,在这里问问题也有帮助,没有一个快速简单的名称来搜索您的确切问题确实使您很难弄清楚哪里出了问题。:)无论如何,祝您好运,并且对您努力测试代码也有好处。


还有其他方法,但是鉴于我在SO上看过的其他问题/答案似乎无济于事,所以我希望这一方法能解决这一问题。其他参考:

当然,它剥开重复这一切可以用做unittest.mockPython中可供3.3+或PyPI上的原/滚动反向移植版本,但考虑到这些库隐藏了一些什么实际发生的错综复杂的,他们可能会隐藏一些的有关实际发生(或需要发生)或重定向实际发生方式的详细信息。如果需要,您可以继续阅读unittest.mock.patch并略微了解StringIO补丁程序sys.stdout部分。