python在单元测试中模拟raw_input

Posted

技术标签:

【中文标题】python在单元测试中模拟raw_input【英文标题】:python mocking raw input in unittests 【发布时间】:2014-01-29 13:37:45 【问题描述】:

假设我有这个 python 代码:

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

如何为此编写单元测试?我知道我必须使用“模拟”,但我不明白如何。谁能举个简单的例子?

【问题讨论】:

supply inputs to python unittests 的可能重复项 我在那里找不到答案 三个答案之一是字面意思关于使用mock 测试raw_input 【参考方案1】:

我正在使用 Python 3.4,并且必须调整上面的答案。我的解决方案将通用代码分解到自定义 runTest 方法中,并向您展示如何修补 input()print()。这是广告中的代码:

import unittest
from io import StringIO
from unittest.mock import patch


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()

【讨论】:

我使用nose2(不直接使用unittest)为我的设置调整了您的答案,这对我来说效果很好。需要注意的一件事是,如果将 fakeout.getvalue() 更改为 fakeout.getvalue().strip(),则可以避免传递额外的换行符。【参考方案2】:

这是我在 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


【讨论】:

【参考方案3】:

您无法修补输入,但可以将其包装以使用 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')

请记住,此 sn-p 仅适用于 Python 3.3+ 版本

【讨论】:

@ArtOfWarfare mock 是python3.3 中的新功能docs.python.org/3/library/unittest.mock.html 有一个后向端口pypi.python.org/pypi/mock 您应该在答案中指定 python 版本。谢谢@gawel 您不需要包装输入。 @patch('builtins.input', return_value='yes') 应该可以解决问题。 @patch('builtins.input', return_value='yes') 不是一个好主意,因为它会导致 pdb.set_trace() 崩溃。 为什么需要在def test_answer_yes(self, input): 中添加输入作为第二个参数?【参考方案4】:

刚刚遇到同样的问题,但我只是嘲笑__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')

【讨论】:

【参考方案5】:

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

    raw_input(输入副作用)需要被模拟。 print(输出副作用)需要检查。

在理想的单元测试函数中,不会有副作用。只需提交参数即可测试函数,并检查其输出。但是我们经常想在像你这样的函数中测试不理想的函数,IE。

那么我们该怎么办?好吧,在 Python 3.3 中,我上面列出的两个问题都变得微不足道,因为 unittest 模块获得了模拟和检查副作用的能力。但是,截至 2014 年初,只有 30% 的 Python 程序员转向了 3.x,所以为了其他 70% 的 Python 程序员仍在使用 2.x,我将概述一个答案。按照目前的速度,3.x 直到 2019 年才会超过 2.x,而 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')

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

希望您能欣赏我写的所有这些诡计,因为我今晚花了大约 90 分钟来整理。我测试了所有这些代码,并验证它们都适用于 Python 2.7。

【讨论】:

感谢您的出色回答。 python 3 的更新,mockRawInput 上下文管理器:import builtins @contextmanager def mockRawInput(mock): original_raw_input = builtins.input builtins.input = lambda _: mock yield builtins.input = original_raw_input 【参考方案6】:
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

【讨论】:

以上是关于python在单元测试中模拟raw_input的主要内容,如果未能解决你的问题,请参考以下文章

使用模拟 MongoDB 服务器进行单元测试

python mock模块使用

在 SwiftUI 单元测试中禁用模拟器

python unittest 之mock

单元测试——使用模拟对象做交互测试

在 Typescript 单元测试中模拟