在Python单元测试中模拟类属性的更好方法

59

我有一个基类,定义了一个类属性和一些依赖于它的子类,例如:

class Base(object):
    assignment = dict(a=1, b=2, c=3)

我希望能够使用不同的分配(例如空字典、单个项目等)对这个类进行单元测试。当然,这极其简化,不是重构我的类或测试的问题。

我想到的(pytest)测试最终是可行的。

from .base import Base

def test_empty(self):
    with mock.patch("base.Base.assignment") as a:
        a.__get__ = mock.Mock(return_value={})
        assert len(Base().assignment.values()) == 0

def test_single(self):
    with mock.patch("base.Base.assignment") as a:
        a.__get__ = mock.Mock(return_value={'a':1})
        assert len(Base().assignment.values()) == 1

这感觉相当复杂和hacky - 我甚至不完全理解它为什么有效(虽然我熟悉描述符)。mock是否自动将类属性转换为描述符?

一个更合乎逻辑的解决方案并不起作用:

def test_single(self):
    with mock.patch("base.Base") as a:
        a.assignment = mock.PropertyMock(return_value={'a':1})
        assert len(Base().assignment.values()) == 1

或者只是

def test_single(self):
    with mock.patch("base.Base") as a:
        a.assignment = {'a':1}
        assert len(Base().assignment.values()) == 1

我尝试过的其他变体也都不起作用(测试中分配保持不变)。

模拟类属性的正确方法是什么?有比上面这种更好/更易懂的方法吗?

6个回答

53

base.Base.assignment 只需被替换为一个 Mock 对象。通过添加一个 __get__ 方法,你使它成为了一个描述器。

这有点冗长而且有点不必要; 你可以直接设置 base.Base.assignment:

def test_empty(self):
    Base.assignment = {}
    assert len(Base().assignment.values()) == 0

当使用测试并发时,这样做并不太安全。

要使用PropertyMock,我会这样做:

with patch('base.Base.assignment', new_callable=PropertyMock) as a:
    a.return_value = {'a': 1}

或者甚至:

with patch('base.Base.assignment', new_callable=PropertyMock, 
           return_value={'a': 1}):

@IvovanderWijk:Bar.assignment 是一个模拟吗? - Martijn Pieters
@IvovanderWijk:我很惊讶PropertyMock没有起作用。 - Martijn Pieters
无论是部分模拟 Bar 还是仅模拟“assignment”属性,都可以使用模拟模块提供的方法。 - Ivo van der Wijk
new_callable 是一个不错的建议。PropertyMock(return_value={'a':1}) 更好 :) (不再需要“作为”或进一步的赋值) - Ivo van der Wijk
@IvovanderWijk:至于Bar.assignment.__get__ = lambda: {'a': 1};那是行不通的,因为__get__ 方法需要接受selfobjtype_参数。Mock对象至少对其可调用函数使用了*args, **kwargs签名。 - Martijn Pieters
显示剩余5条评论

19

也许我漏掉了什么,但不使用 PropertyMock 可能实现这一点吗?

with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
   # do stuff

这里的第三个位置参数是 new= 关键字参数。它看起来作为一个位置参数非常好,但是带有超过两个位置参数的函数调用总是让我感到有点紧张... - LondonRob
这个程序可以正常工作的事实让我想PropertyMock也许不需要存在?? - LondonRob

12

为了提高可读性,您可以使用@patch装饰器:

from mock import patch
from unittest import TestCase

from base import Base

class MyTest(TestCase):
    @patch('base.Base.assignment')
    def test_empty(self, mock_assignment):
        # The `mock_assignment` is a MagicMock instance,
        # you can do whatever you want to it.
        mock_assignment.__get__.return_value = {}

        self.assertEqual(len(Base().assignment.values()), 0)
        # ... and so on

你可以在http://www.voidspace.org.uk/python/mock/patch.html#mock.patch找到更多信息。


好的观点。一开始没有让修饰符与pytest一起工作(它与pytest的fixture参数'injection'发生了冲突),但事实证明这只是一个适当参数顺序的问题(先进行补丁)。 - Ivo van der Wijk
从mock-1.0.1版本开始,这似乎不再是一个问题了:https://bitbucket.org/hpk42/pytest/issue/217/mockpatch-decorator-and-test-fixtures-don - Dan Keder

7

如果你的类(例如Queue)已经被导入到你的测试中,而你想要修补MAX_RETRY属性,你可以使用@patch.object或者更好地使用@patch.multiple

from mock import patch, PropertyMock, Mock
from somewhere import Queue

@patch.multiple(Queue, MAX_RETRY=1, some_class_method=Mock)
def test_something(self):
    do_something()


@patch.object(Queue, 'MAX_RETRY', return_value=1, new_callable=PropertyMock)
def test_something(self, _mocked):
    do_something()

7
以下是关于如何对您的Base类进行单元测试的示例:
  • 模拟不同类型(例如dictint)的多个类属性
  • 使用@patch装饰器和pytest框架,支持python 2.7+3+版本。

# -*- coding: utf-8 -*-
try: #python 3
    from unittest.mock import patch, PropertyMock
except ImportError as e: #python 2
    from mock import patch, PropertyMock 

from base import Base

@patch('base.Base.assign_dict', new_callable=PropertyMock, return_value=dict(a=1, b=2, c=3))
@patch('base.Base.assign_int',  new_callable=PropertyMock, return_value=9765)
def test_type(mock_dict, mock_int):
    """Test if mocked class attributes have correct types"""
    assert isinstance(Base().assign_dict, dict)
    assert isinstance(Base().assign_int , int)

0

这些答案似乎遗漏了一些内容。

在我的情况下,我有一个简单的文件,在顶部有一些常量,像这样:

LIB_DIR_PATH_STR = 'some_path_to_module'

接下来我有一个方法,在导入这个库之前,我会将它添加到sys.path中:

def main():
    ...
    sys.path.append(LIB_DIR_PATH_STR)

... 但是我在测试中想要模拟 LIB_DIR_PATH_STR,使其指向一个不存在的路径,即用于错误处理。这里我们不讨论模拟脚本中的任何类或方法。

然而,事实证明这是可能的(前提是之前已经导入了 my_script):

with mock.patch.object(my_script, 'LIB_DIR_PATH_STR', new_callable=mock.PropertyMock(return_value=non_existent_dir_path_str)):
    my_script.main()

...即PropertyMock可以使用其自己的return_value实例化。这与为参与的PropertyMock指定return_value不同(补丁的类将是Mock或者MagicMock)。后一种方法对于这种简单的“用另一个字符串替换字符串”的模拟根本行不通:pytest会抱怨“期望字符串但得到了Mock”。

我的具体示例与问题(类属性)无关,只是为了展示如何完成。但它也可以用于类属性...


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