Лучший способ обмануть атрибут класса в python unit test

У меня есть базовый класс, который определяет атрибут класса и некоторые дочерние классы, которые зависят от него, например

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

Это кажется довольно сложным и взломанным - я даже не понимаю, почему он работает (хотя я знаком с дескрипторами). Может ли макет автоматически преобразовывать атрибуты класса в дескрипторы?

Решение, которое будет более логичным, не работает:

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

Другие варианты, которые я пробовал, тоже не работают (назначения не изменяются в тесте).

Каков правильный способ издеваться над атрибутом класса? Есть ли лучший/более понятный способ, чем выше?

Ответы

Ответ 1

base.Base.assignment просто заменяется объектом Mock. Вы сделали его дескриптором, добавив метод __get__.

Это немного подробный и немного лишний; вы можете просто установить base.Base.assignment напрямую:

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

Это не слишком безопасно при использовании теста concurrency, конечно.

Чтобы использовать 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}):

Ответ 2

Чтобы улучшить читаемость, вы можете использовать декоратор @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.

Ответ 3

Если ваш класс (например, Queue) уже импортирован внутри вашего теста - и вы хотите изменить MAX_RETRY attr - вы можете использовать @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()

Ответ 4

Вот пример, как выполнить unit тест класса Base:

  • mocking несколько атрибутов класса разных типов (например: dict и int)
  • с помощью декоратора @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)

Ответ 5

Возможно, мне что-то не хватает, но разве это невозможно без использования PropertyMock?

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