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

时间:2016-08-09 21:59:05

标签: python io python-unittest

我正在尝试测试一个Python脚本(2.7),我使用标准输入(使用raw_input()进行处理并使用简单的打印进行编写)但我不知道如何做到这一点我确信这个问题很简单。

这是我脚本的一个非常非常非常简洁的代码:

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)

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

1 个答案:

答案 0 :(得分: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

在我最近的一个项目中,我已经为stdin做了这个,在那里用{(1}}输入用户(或其他程序通过管道)作为stdin进入你的输入:

str

至于stdout和stderr:

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

    def cleanup():
        sys.stdin = stdin

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

请注意,在这两种情况下,它都接受一个测试用例实例,并调用其addCleanup方法,该方法会添加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() 函数调用,将其重置回测试方法持续时间的位置。结束了。结果是,在测试用例中调用此操作的持续时间到最后,cleanup和朋友将被替换为sys.stdout版本,这意味着您可以轻松检查其值,并且不要# 39;不得不担心会留下一团糟。

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

io.StringIO

现在,在Python 2中,只有class ExampleTestCase(unittest.TestCase): def test_example(self): stub_stdin(self, '42') stub_stdouts(self) example() self.assertEqual(sys.stdout.getvalue(), '42\n') 类来自StringIO模块才会通过此测试,而在Python 3中则不存在此类模块。您可以做的是使用StringIO模块中的版本进行修改,使其在接受的输入方面稍微宽松一些,这样unicode编码/解码将自动完成而不是触发异常(如果没有以下内容,Python 2中的io语句将无法很好地工作。我通常这样做是为了Python 2和3之间的交叉兼容性:

print

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

还有一点需要注意:如果每个测试方法都需要print 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)

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

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

所以是的,对我所做的事情的一些解释可能有所帮助:记住在TestCase类中,框架严格依赖于实例assertEqual和朋友才能运行。因此,为了确保在正确的级别上进行测试,您确实希望始终调用这些断言,以便在错误发生时显示有用的错误消息,输入/答案并未完全显示,而不是像你对for循环所做的那样直到最后(这会告诉你一些错误的东西,但不是几百个中的哪个,现在你很生气)。同样是helper方法 - 您可以将其称为任何您想要的,只要它不以test开头,因为框架将尝试将其作为一个运行,并且它将非常失败。所以只需遵循这个约定,你就可以在测试用例中使用模板来运行你的测试 - 然后你就可以在一个带有一堆输入/输出的循环中使用它,就像我做的那样。

至于你的另一个问题:

  

只有我在导入StringIO时遇到了一些问题,因为我正在进行导入StringIO,我需要从StringIO导入StringIO导入(我不明白为什么),但是就像它可能一样,它有效。

好吧,如果你查看我原来的代码,我确实告诉你import io是怎么做的,然后通过定义io.StringIO来覆盖class StringIO(io.StringIO)类。但它适用于你,因为你是严格按照Python 2进行的,而我确实试图在可能的情况下将Python 3的答案作为目标,因为Python 2(可能肯定这次)不会在不到5年的时间内得到支持。想想可能正在阅读此帖的未来用户,他们遇到了与您类似的问题。无论如何,原来的from StringIO import StringIO是有效的,因为StringIO模块中的StringIO类。虽然from cStringIO import StringIO应该可以导入C模块的StringIO版本。它的工作原理是因为它们都提供了足够接近的接口,所以它们基本上可以正常工作(当然你试着在Python 3下运行它)。

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

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

当然,它重复使用Python 3.3+或unittest.mock中提供的original/rolling backport version on pypi来完成所有这些 ,但是假设这些库隐藏了一些对实际发生的事情的复杂性,他们最终可能会隐藏一些实际发生(或需要发生)的细节或重定向实际发生的方式。如果您愿意,可以阅读unittest.mock.patch,然后稍微转到StringIO修补sys.stdout部分。