在Python中,如何对没有返回值的函数进行单元测试?

46

我有一些代码,例如:

class HelloTest:

    def foo(self, msg):
        self.bar(msg.upper())

    def bar(self, msg):
        print(msg)

使用标准库unittest,我们可以使用assertEqualassertTrue等方法来验证从函数返回的内容。
由于foo没有明确返回值,我该如何测试它是否执行了正确的操作?
6个回答

20

正如另一个答案所提到的那样,您可以使用Python模拟库来对函数/方法的调用进行断言。

from mock import patch
from my_module import HelloTest
import unittest

class TestFoo(unittest.TestCase):

    @patch('hello.HelloTest.bar')
    def test_foo_case(self, mock_bar):

        ht = HelloTest()

        ht.foo("some string")
        self.assertTrue(mock_bar.called)
        self.assertEqual(mock_bar.call_args[0][0], "SOME STRING")

此补丁撤销了HelloTest类中的bar方法,并用记录其调用的模拟对象替换它。

模拟是一个有些棘手的问题。只有在必要时才这样做,因为它会使您的测试变得脆弱。例如,您可能永远不会注意到模拟对象的API更改。


self.assertEqual(ob.msg, "SOME STRING") 这一行中,ob 是从哪里来的? - Isaac Sekamatte
4
你知道吗...我真的不知道?看起来我六年前犯了一个错误 :-) 应该是这样的: self.assertEqual(mock_bar.call_args[0][0], "一些字符串") 我会更新答案。 - aychedee

19

我不太明白为什么每个人都想要检查foo是否调用了bar

foo有一些功能,这些功能需要进行测试。如果foo使用bar来实现其功能,那就不应该是我们关心的事情。我们应该测试foo(msg)是否将msg.upper()写入sys.stdout

你可以sys.stdout重定向到一个字符串缓冲区,然后检查该缓冲区的内容是否与你期望的相符。

示例:

import io
import sys
import unittest


class TestScript(unittest.TestCase):

    def setUp(self):
        self._old_stdout = sys.stdout
        sys.stdout = io.TextIOWrapper(io.BytesIO(), sys.stdout.encoding)

    def test_foo(self):
        hello_test = HelloTest()
        hello_test.foo('hello')
        sys.stdout.seek(0)
        self.assertEqual(sys.stdout.read(), 'HELLO\n')

    def tearDown(self):
        sys.stdout.close()
        sys.stdout = self._old_stdout


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

对于一个版本,其中 sys.stdout.write() 同时接受 Unicode 和字节字符串,请参见 this answer


10
在这种情况下,我会模拟打印,然后在我的断言中使用该模拟。
在Python中,您将使用Mock包进行模拟。

2
如果他使用的Python版本低于3,那么模拟打印并不是那么简单。他可以模拟sys.stdout,但他必须更改bar。 - aychedee
1
如果问题是如何测试“bar”,那么这个答案是可行的,但如果我们正在测试“foo”,那么它就不行,因为“foo”没有调用“print”。 “foo”的功能是使用大写的消息调用“bar”。@aychedee的答案是正确的,因为它只测试“foo”的功能,而不是“bar”的功能。 - bgordon

9
感谢 @Jordan 的介绍,我编写了这个单元测试,并认为它可以用于测试 HelloTest.foo。
from mock import Mock
import unittest


class HelloTestTestCase(unittest.TestCase):
    def setUp(self):
        self.hello_test = HelloTest()

    def tearDown(self):
        pass

    def test_foo(self):
        msg = 'hello'
        expected_bar_arg = 'HELLO'
        self.hello_test.bar = Mock()

        self.hello_test.foo(msg)
        self.hello_test.bar.assert_called_once_with(expected_bar_arg)


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

5

以下是您的代码,可以完成与上述相同的任务:

class HelloTest(object):

    def foo(self, msg):
        self.msg = msg.upper()
        self.bar()

    def bar(self):
        print self.msg

单元测试是:

from hello import HelloTest
import unittest

class TestFoo(unittest.TestCase):
    def test_foo_case(self):
        msg = "test"
        ob = HelloTest()
        ob.foo(msg)
        expected = "TEST"
        self.assertEqual(ob.msg, expected)

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

这是测试某些事物。尽管如此,这不一定是需要测试的内容。 - chepner

4
在Python 3中,你可以告诉print函数输出到哪里

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

因此,你可以添加一个可选参数:

def bar(self, MSG, file=sys.stdout):
    print(MSG, file=file)

通常情况下,它会输出到标准输出(stdout),但你可以在单元测试中传递自己的文件。

在Python 2中,这有点麻烦,但你可以将stdout重定向到StringIO缓冲区:(参考链接)

import StringIO
import sys

out = StringIO.StringIO()
sys.stdout = out

# run unit tests

sys.stdout = sys.__stdout__

# check the contents of `out`

1
针对带输出功能的函数进行单元测试的好解决方案。但我最关心的是如何测试 def foo(self, msg),因为并不是所有函数都会与标准输出交互。 - Yarkee

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