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

62 投票
6 回答
105979 浏览
提问于 2025-04-17 21:46

我有一个基础类,这个类定义了一个类属性,还有一些依赖于这个属性的子类,比如:

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

这感觉有点复杂和不太靠谱——我甚至不完全理解为什么它能工作(不过我对描述符有一些了解)。难道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 个回答

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

19

我可能理解错了,但是不是可以不使用 PropertyMock 就实现这个功能呢?

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

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}):

撰写回答