58

I have a base class that defines a class attribute and some child classes that depend on it, e.g.

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

I want to unittest this class with different assignments, e.g. empty dictionary, single item, etc. This is extremely simplified of course, it's not a matter of refactoring my classes or tests

The (pytest) tests I have come up with, eventually, that work are

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

This feels rather complicated and hacky - I don't even fully understand why it works (I am familiar with descriptors though). Does mock automagically transform class attributes into descriptors?

A solution that would feel more logical does not work:

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

or just

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

Other variants that I've tried don't work either (assignments remains unchanged in the test).

What's the proper way to mock a class attribute? Is there a better / more understandable way than the one above?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Ivo van der Wijk
  • 16,341
  • 4
  • 43
  • 57

6 Answers6

52

base.Base.assignment is simply replaced with a Mock object. You made it a descriptor by adding a __get__ method.

It's a little verbose and a little unnecessary; you could simply set base.Base.assignment directly:

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

This isn't too safe when using test concurrency, of course.

To use a PropertyMock, I'd use:

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

or even:

with patch('base.Base.assignment', new_callable=PropertyMock, 
           return_value={'a': 1}):
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Bar.assignment.__get__ = lambda: {1:1} wouldn't have worked here (just tried), so mock injects/mocks a descriptor. Also, mock takes care of restoring the 'old' definition which avoids nasty side effects when modifying globally this way. I can do some old school hacking around like you suggest (and I use to) but I want to learn the 'mock' way :) – Ivo van der Wijk Mar 11 '14 at 11:53
  • @IvovanderWijk: With `Bar.assignment` being a mock? – Martijn Pieters Mar 11 '14 at 11:55
  • @IvovanderWijk: I am surprised that `PropertyMock` didn't work though. – Martijn Pieters Mar 11 '14 at 11:56
  • Either by partially mocking Bar or by only mocking the 'assignment' attribute, whatever the mock module provides. – Ivo van der Wijk Mar 11 '14 at 11:56
  • new_callable is a good suggestion. PropertyMock(return_value={'a':1}) makes it even better :) (no need for the 'as a' or further assignment anymore) – Ivo van der Wijk Mar 11 '14 at 12:16
  • @IvovanderWijk: as for `Bar.assignment.__get__ = lambda: {'a': 1}`; that won't work because the `__get__` *method* would need to accept `self`, `obj` and `type_` arguments.. The `Mock` object at least uses `*args, **kwargs` signatures for their callables. – Martijn Pieters Mar 11 '14 at 12:18
  • No, python refuses the assignment: AttributeError: 'dict' object has no attribute '__get__' – Ivo van der Wijk Mar 11 '14 at 12:20
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/49479/discussion-between-ivo-van-der-wijk-and-martijn-pieters) – Ivo van der Wijk Mar 11 '14 at 12:20
  • @IvovanderWijk: That'd be correct, because `dict()` objects do not support setting additional attributes (there is no `.__dict__` attribute on dictionaries). – Martijn Pieters Mar 11 '14 at 12:24
  • Thank you so much! This answer helped me somuch! – Sush Sep 26 '18 at 20:32
18

Perhaps I'm missing something, but isn't this possible without using PropertyMock?

with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
   # do stuff
igniteflow
  • 8,404
  • 10
  • 38
  • 46
  • The third positional argument here is the `new=` keyword argument. It actually looks pretty good as a positional argument, but function calls with more than two positional arguments always make me feel a bit nervous.... – LondonRob Jan 25 '22 at 18:52
  • The fact that this works does make me think that `PropertyMock` might not need to exist?? – LondonRob Jan 25 '22 at 18:54
11

To improve readability you can use the @patch decorator:

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

You can find more details at http://www.voidspace.org.uk/python/mock/patch.html#mock.patch.

Dan Keder
  • 684
  • 5
  • 8
  • Good point. Didn't get the decorated to work with pytest at first (it conflicted with pytest's fixture argument 'injection') but it turns out to be a matter of proper argument order (patches go first) – Ivo van der Wijk Mar 12 '14 at 12:03
  • It seems that since mock-1.0.1 it isn't an issue anymore: https://bitbucket.org/hpk42/pytest/issue/217/mockpatch-decorator-and-test-fixtures-don – Dan Keder Mar 12 '14 at 12:06
6

If your class (Queue for example) in already imported inside your test - and you want to patch MAX_RETRY attr - you can use @patch.object or simply better @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()
pymen
  • 5,737
  • 44
  • 35
6

Here is an example how to unit-test your Base class:

  • mocking multiple class attributes of different types (ie: dict and int)
  • using the @patch decorator and pytest framework with with python 2.7+ or 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)
x0s
  • 1,648
  • 17
  • 17
0

These answers seem to have missed something.

In my case I had a simple file with some constants at the top, like this:

LIB_DIR_PATH_STR = 'some_path_to_module'

After this I have a method during which I add this library to sys.path prior to importing it:

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

... but what I wanted to do in testing is to mock LIB_DIR_PATH_STR, so that it points to a non-existent path, i.e. for error-handling. Here we're not talking about mocking any classes or even methods in a script.

However, it turns out that it is possible (where my_script has previously been imported):

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

... i.e. PropertyMock can be instantiated with a return_value of its own. The is not the same as specifying the return_value for a patch in which a PropertyMock is participating (the class of the patch will then be Mock or maybe MagicMock). The latter approach simply won't work for this simple "replace a string with another" type of mock: pytest will complain "expected string but got Mock".

My specific example is tangential to the question (class attributes), to show how it's done. But it can be used with class attributes too...

mike rodent
  • 14,126
  • 11
  • 103
  • 157