python在单元测试中模拟raw_input

时间:2014-01-10 14:23:46

标签: python unit-testing mocking

假设我有这个python代码:

def answer():
    ans = raw_input('enter yes or no')
    if ans == 'yes':
        print 'you entered yes'
    if ans == 'no':
        print 'you entered no'

我如何为此编写单元测试?我知道我必须使用'模拟',但我不明白如何。有人可以做一些简单的例子吗?

6 个答案:

答案 0 :(得分:36)

您无法修改输入,但可以将其包装为使用mock.patch()。这是一个解决方案:

from unittest.mock import patch
from unittest import TestCase


def get_input(text):
    return input(text)


def answer():
    ans = get_input('enter yes or no')
    if ans == 'yes':
        return 'you entered yes'
    if ans == 'no':
        return 'you entered no'


class Test(TestCase):

    # get_input will return 'yes' during this test
    @patch('yourmodule.get_input', return_value='yes')
    def test_answer_yes(self, input):
        self.assertEqual(answer(), 'you entered yes')

    @patch('yourmodule.get_input', return_value='no')
    def test_answer_no(self, input):
        self.assertEqual(answer(), 'you entered no')

请注意,此代码段仅适用于Python版本3.3 +

答案 1 :(得分:28)

好的,首先,我觉得有必要指出在原始代码中,实际上有两件事需要解决:

  1. raw_input(输入副作用)需要被模拟。
  2. print(输出副作用)需要进行检查。
  3. 在理想的单元测试功能中,没有副作用。只需通过处理参数来测试函数,就会检查其输出。但是我们常常想要在像你这样的函数中测试不理想的函数IE。

    那我们该怎么办?好吧,在Python 3.3中,我上面列出的两个问题变得微不足道,因为unittest模块获得了模拟和检查副作用的能力。但是,截至2014年初,只有30%的Python程序员已经转向3.x,所以为了其他70%的Python程序员仍然使用2.x,我将概述一个答案。按照目前的速度,3.x不会超过2.x直到~209,并且2.x不会消失直到~2027。所以我认为这个答案将在未来几年内发挥作用。

    我想一次解决上面列出的问题,因此我最初会将您的功能从使用print作为其输出更改为使用return。没有惊喜,这里有代码:

    def answerReturn():
        ans = raw_input('enter yes or no')
        if ans == 'yes':
            return 'you entered yes'
        if ans == 'no':
            return 'you entered no'
    

    所以我们需要做的就是模拟raw_input。很简单 - Omid Raha's answer to this very question向我们展示了如何通过我们的模拟实现来调动__builtins__.raw_input实现。除了他的回答没有正确地组织成TestCase和函数,所以我将证明这一点。

    import unittest    
    
    class TestAnswerReturn(unittest.TestCase):
        def testYes(self):
            original_raw_input = __builtins__.raw_input
            __builtins__.raw_input = lambda _: 'yes'
            self.assertEqual(answerReturn(), 'you entered yes')
            __builtins__.raw_input = original_raw_input
    
        def testNo(self):
            original_raw_input = __builtins__.raw_input
            __builtins__.raw_input = lambda _: 'no'
            self.assertEqual(answerReturn(), 'you entered no')
            __builtins__.raw_input = original_raw_input
    

    关于Python命名约定的小注释 - 解析器所需但未使用的变量通常命名为_,就像lambda的未使用变量(通常是提示符所示)一样对于raw_input的用户,如果你想知道为什么在这种情况下根本需要它。)

    无论如何,这是混乱和多余的。因此,我将通过添加contextmanager来删除重复,这将允许简单的with语句。

    from contextlib import contextmanager
    
    @contextmanager
    def mockRawInput(mock):
        original_raw_input = __builtins__.raw_input
        __builtins__.raw_input = lambda _: mock
        yield
        __builtins__.raw_input = original_raw_input
    
    class TestAnswerReturn(unittest.TestCase):
        def testYes(self):
            with mockRawInput('yes'):
                self.assertEqual(answerReturn(), 'you entered yes')
    
        def testNo(self):
            with mockRawInput('no'):
                self.assertEqual(answerReturn(), 'you entered no')
    

    我认为很好地回答了第一部分。在第二部分 - 检查print。我觉得这很棘手 - 我很想知道是否有人有更好的答案。

    无论如何,print语句无法覆盖,但如果您使用print()函数(如您所愿)和from __future__ import print_function,则可以使用以下内容:

    class PromiseString(str):
        def set(self, newString):
            self.innerString = newString
    
        def __eq__(self, other):
            return self.innerString == other
    
    @contextmanager
    def getPrint():
        promise = PromiseString()
        original_print = __builtin__.print
        __builtin__.print = lambda message: promise.set(message)
        yield promise
        __builtin__.print = original_print
    
    class TestAnswer(unittest.TestCase):
        def testYes(self):
            with mockRawInput('yes'), getPrint() as response:
                answer()
                self.assertEqual(response, 'you entered yes')
    
        def testNo(self):
            with mockRawInput('no'), getPrint() as response:
                answer()
                self.assertEqual(response, 'you entered no')
    

    这里棘手的一点是,在输入yield块之前,您需要with响应。但是,在调用print()块中的with之前,您无法知道该响应是什么。如果字符串是可变的,这将是好的,但它们不是。因此,提出了一个小承诺或代理类 - PromiseString。它只做两件事 - 允许设置一个字符串(或任何东西,真的),让我们知道它是否等于不同的字符串。 PromiseStringyield,然后设置为print块中通常为with的值。

    希望你能理解我写的所有这些诡计,因为我今晚花了大约90分钟才把它拼凑起来。我测试了所有这些代码,并验证了它都适用于Python 2.7。

答案 2 :(得分:7)

我正在使用Python 3.4并且必须调整上面的答案。我的解决方案将常用代码纳入自定义runTest方法,并向您展示如何修补input()print()。这里运行的代码如下:     进口单位测试     来自io import StringIO     来自unittest.mock导入补丁

def answer():
    ans = input('enter yes or no')
    if ans == 'yes':
        print('you entered yes')
    if ans == 'no':
        print('you entered no')


class MyTestCase(unittest.TestCase):
    def runTest(self, given_answer, expected_out):
        with patch('builtins.input', return_value=given_answer), patch('sys.stdout', new=StringIO()) as fake_out:
            answer()
            self.assertEqual(fake_out.getvalue().strip(), expected_out)

    def testNo(self):
        self.runTest('no', 'you entered no')

    def testYes(self):
        self.runTest('yes', 'you entered yes')

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

答案 3 :(得分:6)

刚遇到同样的问题,但我只是嘲笑__builtin__.raw_input

仅在Python 2上进行测试pip install mock如果您尚未安装软件包。

from mock import patch
from unittest import TestCase

class TestAnswer(TestCase):
    def test_yes(self):
        with patch('__builtin__.raw_input', return_value='yes') as _raw_input:
            self.assertEqual(answer(), 'you entered yes')
            _raw_input.assert_called_once_with('enter yes or no')

    def test_no(self):
        with patch('__builtin__.raw_input', return_value='no') as _raw_input:
            self.assertEqual(answer(), 'you entered no')
            _raw_input.assert_called_once_with('enter yes or no')

或者,使用库genty,您可以简化两个测试:

from genty import genty, genty_dataset
from mock import patch
from unittest import TestCase

@genty
class TestAnswer(TestCase):
    @genty_dataset(
        ('yes', 'you entered yes'),
        ('no', 'you entered no'),
    )
    def test_answer(self, expected_input, expected_answer):
        with patch('__builtin__.raw_input', return_value=expected_input) as _raw_input:
            self.assertEqual(answer(), expected_answer)
            _raw_input.assert_called_once_with('enter yes or no')

答案 4 :(得分:0)

def answer():
    ans = raw_input('enter yes or no')
    if ans == 'yes':
        return 'you entered yes'
    if ans == 'no':
        return 'you entered no'


def test_answer_yes():
    assert(answer() == 'you entered yes')

def test_answer_no():
    assert(answer() == 'you entered no')

origin_raw_input = __builtins__.raw_input

__builtins__.raw_input = lambda x: "yes"
test_answer_yes()

__builtins__.raw_input = lambda x: "no"
test_answer_no()

__builtins__.raw_input = origin_raw_input

答案 5 :(得分:0)

这是我在 Python 3 中所做的事情:

class MockInputFunction:
    def __init__(self, return_value=None):
        self.return_value = return_value
        self._orig_input_fn = __builtins__['input']

    def _mock_input_fn(self, prompt):
        print(prompt + str(self.return_value))
        return self.return_value

    def __enter__(self):
        __builtins__['input'] = self._mock_input_fn

    def __exit__(self, type, value, traceback):
        __builtins__['input'] = self._orig_input_fn

然后可以在任何上下文中使用。例如,pytest使用普通的assert语句。

def func():
    """ function to test """
    x = input("What is x? ")
    return int(x)

# to test, you could simply do:
with MockInputFunction(return_value=13):
    assert func() == 13