如何模拟输入以供pyunit使用?

28

我正在尝试测试一个从 stdin 获取输入的函数,目前我正在使用以下方式进行测试:

cat /usr/share/dict/words | ./spellchecker.py

为了进行测试自动化,有没有办法让pyunit模拟用户输入到raw_input()中?


2
你可能想尝试为依赖注入构建你的应用程序,就像这个答案中对类似问题的回答一样。 - Greg Haskins
3
请将此文本翻译成中文:http://uuoc.com/ ;-) (应该是 ./spellchecker.py < /usr/share/dict/words - johnsyweb
5个回答

25

简短回答是通过Monkey Patch来修改raw_input()函数。

How to display the redirected stdin in Python?问题的答案中有一些很好的例子。

下面是一个简单的、微不足道的例子,使用了一个lambda来丢弃提示并返回我们想要的结果。

被测试系统

cat ./name_getter.py
#!/usr/bin/env python

class NameGetter(object):

    def get_name(self):
        self.name = raw_input('What is your name? ')

    def greet(self):
        print 'Hello, ', self.name, '!'

    def run(self):
        self.get_name()
        self.greet()

if __name__ == '__main__':
    ng = NameGetter()
    ng.run()

$ echo Derek | ./name_getter.py 
What is your name? Hello,  Derek !

测试用例:

$ cat ./t_name_getter.py
#!/usr/bin/env python

import unittest
import name_getter

class TestNameGetter(unittest.TestCase):

    def test_get_alice(self):
        name_getter.raw_input = lambda _: 'Alice'
        ng = name_getter.NameGetter()
        ng.get_name()
        self.assertEquals(ng.name, 'Alice')

    def test_get_bob(self):
        name_getter.raw_input = lambda _: 'Bob'
        ng = name_getter.NameGetter()
        ng.get_name()
        self.assertEquals(ng.name, 'Bob')

if __name__ == '__main__':
    unittest.main()

$ ./t_name_getter.py -v
test_get_alice (__main__.TestNameGetter) ... ok
test_get_bob (__main__.TestNameGetter) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

1
抱歉,name_getter.raw_inputNameGetter 中定义在哪里? - Alper
1
@Alper 不是这样的。测试用例对 name_getter 模块进行了猴子补丁,修改了内置函数 raw_input() - johnsyweb

19

更新 -- 使用unittest.mock.patch

自Python 3.3以来,有一个新的子模块unittest被称为mock,可以完全满足您的需求。对于使用Python 2.6或更高版本的用户,可以在此处找到mock的后移版本。

import unittest
from unittest.mock import patch

import module_under_test


class MyTestCase(unittest.TestCase):

    def setUp(self):
        # raw_input is untouched before test
        assert module_under_test.raw_input is __builtins__.raw_input

    def test_using_with(self):
        input_data = "123"
        expected = int(input_data)

        with patch.object(module_under_test, "raw_input", create=True, 
                return_value=expected):
            # create=True is needed as raw_input is not in the globals of 
            # module_under_test, but actually found in __builtins__ .
            actual = module_under_test.function()

        self.assertEqual(expected, actual)

    @patch.object(module_under_test, "raw_input", create=True)
    def test_using_decorator(self, raw_input):
        raw_input.return_value = input_data = "123"
        expected = int(input_data)

        actual = module_under_test.function()

        self.assertEqual(expected, actual)

    def tearDown(self):
        # raw input is restored after test
        assert module_under_test.raw_input is __builtins__.raw_input

if __name__ == "__main__":
    unittest.main()
# where module_under_test.function is:
def function():
    return int(raw_input("prompt> "))

之前的回答 -- 替换 sys.stdin

我认为你可能需要使用 sys 模块。

你可以像这样做:

import sys

# save actual stdin in case we need it again later
stdin = sys.stdin

sys.stdin = open('simulatedInput.txt','r') 
# or whatever else you want to provide the input eg. StringIO

每次调用raw_input函数都将从simulatedInput.txt中读取内容。如果simulatedInput的内容为

hello
bob

那么第一次调用raw_input会返回"hello",第二次会返回"bob",第三次由于没有更多的文本可读,将抛出EOFError。


1
单元测试不应访问文件系统。 - johnsyweb
4
为什么不呢?可否详细说明并让我了解一下?那么如何测试文件操作函数呢,使用stringIO代替真实文件会更好吗?如果是的话,怎样更好呢?(除了比磁盘IO更快之外,但数据迟早都来自磁盘,这样你可以将测试数据与测试代码分开。请注意不要改变原意。) - ted
@ted:你提到的代码与数据分离是个很好的点,不过你的问题很难以通用方式回答(特别是在别人的答案评论中)。我建议你将其作为一个新问题提出。 - johnsyweb
sys.stdinopen()返回的流并不完全相同。例如,如果您的代码执行stream.tell(),那么它将在此测试中工作,但在真正的sys.stdin中则不会。 - danvk
我认为这应该更新,因为raw_input现在已经从Python API中删除,并被input所取代。 - Iker Jimenez

4
您没有说明spellchecker.py中的代码类型,这使我可以自由推测。
假设它是这样的:
import sys

def check_stdin():
  # some code that uses sys.stdin

为了提高 check_stdin 函数的测试性,我建议将其重构如下:
def check_stdin():
  return check(sys.stdin)

def check(input_stream):
  # same as original code, but instead of
  # sys.stdin it is written it terms of input_stream.

现在大部分的逻辑都在 check 函数中,你可以手工制作任何输入来测试它,而不需要处理 stdin
这是我的意见。

3

sys.stdin替换为StringIO的实例,并通过sys.stdin加载StringIO实例中的数据。此外,sys.__stdin__包含原始的sys.stdin对象,因此在测试后恢复sys.stdin就像sys.stdin = sys.__stdin__这样简单。

Fudge是一个很棒的Python模拟模块,具有方便的装饰器,可以为您执行类似于补丁的操作,并自动清理。你应该去看一下。


如果您必须猴子补丁sys.stdin,则很有可能您正在尝试进行过多的“单元测试”。关于Fudge,请考虑自己实现模拟的好处。 - johnsyweb

2

如果您正在使用mock模块(由Michael Foord编写),为了模拟raw_input函数,您可以使用以下语法:

@patch('src.main.raw_input', create=True, new=MagicMock(return_value='y'))
def test_1(self):
    method_we_try_to_test();    # method or function that calls **raw_input**

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接