如何使用Python Mock断言接受序列参数的调用?

5 投票
2 回答
6637 浏览
提问于 2025-04-17 13:21

我正在处理一系列用户定义的对象。它看起来像下面这样:

class Thing(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

我现在正在测试的方法功能类似于下面这个:

def my_function(things):
    x_calc = calculate_something(t.x for t in things)
    y_calc = calculate_something(t.y for t in things)
    return x_calc / y_calc

我遇到的问题是如何测试对 calculate_something 的调用。我想确认这些调用确实发生了,类似于这样:

calculateSomethingMock.assert_any_call(the_sequence)

我不在乎传给 calculate_something 的顺序,但我确实关心所有元素是否都存在。我可以把生成器函数包裹在一个 set 的调用中,但我觉得我的测试不应该限制传给 calculate_something 的序列类型。我应该能够传入任何类型的序列。另一种选择是创建一个生成序列的方法,而不是使用生成器语法,然后对那个方法进行模拟,但这似乎有点过于复杂。

我应该如何最好地构建这个断言,或者我在这里测试的困难是否说明代码结构不佳?

我使用的是 Python 2.7.3 和 Mock 1.0.1。

(对于任何想要评论的人,我知道我是在最后进行测试,这并不是被认为最好的做法。)

编辑:

在观看了这场精彩的演讲《为什么你不应该使用模拟对象,作者是 Gregory Moeck》后,我重新考虑了是否应该模拟 calculate_something 方法。

2 个回答

7

根据Mock的文档,有一个叫做 call_args_list 的功能,可以满足你的需求。

所以在你的测试中,你需要对 calculate_something 进行Mock处理。

calculate_something = Mock(return_value=None)

当你执行完 my_function 后,可以通过以下方式检查传入的参数:

calculate_something.call_args_list

这样会返回一个列表,里面包含了所有对它的调用记录(以及传入的相应参数)。

编辑

(抱歉让我花了这么久,我得在我的机器上安装Python3.3)

mymodule.py

class Thing:
    ...
def calculate_something:
    ...

def my_function(things):
    # Create the list outside in order to avoid a generator object
    # from being passed to the Mock object.

    xs = [t.x for t in things]
    x_calc = calculate_something(xs)

    ys = [t.y for t in things]
    y_calc = calculate_something(ys)
    return True

test_file.py

import unittest
from unittest.mock import patch, call
import mymodule



class TestFoo(unittest.TestCase):

    # You can patch calculate_something here or
    # do so inside the test body with 
    # mymodule.calcualte_something = Mock()
    @patch('mymodule.calculate_something')
    def test_mock(self, mock_calculate):

        things = [mymodule.Thing(3, 4), mymodule.Thing(7, 8)]

        mymodule.my_function(things)

        # call_args_list returns [call([3, 7]), call([4, 8])]
        callresult = mock_calculate.call_args_list


        # Create our own call() objects to compare against
        xargs = call([3, 7])
        yargs = call([4, 8])

        self.assertEqual(callresult, [xargs, yargs])

        # or
        # we can extract the call() info
        # http://www.voidspace.org.uk/python/mock/helpers.html#mock.call.call_list
        xargs, _ = callresult[0]
        yargs, _ = callresult[1]

        xexpected = [3, 7]
        yexpected = [4, 8]

        self.assertEqual(xargs[0], xexpected)
        self.assertEqual(yargs[0], yexpected)

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

我有一段时间没碰我之前写的代码了,但最近我在重新考虑我的测试方法。我开始更加小心地选择哪些东西需要模拟(mock),哪些不需要。我发现自己无意中开始遵循一个简单的原则:如果模拟某个东西能让我的测试变得更短、更简单,那就去模拟;如果模拟会让测试变得更复杂,那就别碰。对于这个方法来说,简单的输入输出测试就足够了,因为它没有像数据库或文件这样的外部依赖。所以简单来说,我觉得我的问题的答案是:“我不应该模拟 calculate_something。”这样做会让我的测试变得更难读和维护。

撰写回答