Better way to mock class attribute in python unit test
Solution 1
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}):
Solution 2
Perhaps I'm missing something, but isn't this possible without using PropertyMock
?
with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
# do stuff
Solution 3
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.
Solution 4
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()
Solution 5
Here is an example how to unit-test your Base
class:
- mocking multiple class attributes of different types (ie:
dict
andint
) - using the
@patch
decorator andpytest
framework with withpython 2.7+
or3+
.
# -*- 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)
Related videos on Youtube
Comments
-
Ivo van der Wijk almost 4 years
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?
-
Ivo van der Wijk about 10 yearsBar.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 :)
-
Martijn Pieters about 10 years@IvovanderWijk: With
Bar.assignment
being a mock? -
Martijn Pieters about 10 years@IvovanderWijk: I am surprised that
PropertyMock
didn't work though. -
Ivo van der Wijk about 10 yearsEither by partially mocking Bar or by only mocking the 'assignment' attribute, whatever the mock module provides.
-
Ivo van der Wijk about 10 yearsnew_callable is a good suggestion. PropertyMock(return_value={'a':1}) makes it even better :) (no need for the 'as a' or further assignment anymore)
-
Martijn Pieters about 10 years@IvovanderWijk: as for
Bar.assignment.__get__ = lambda: {'a': 1}
; that won't work because the__get__
method would need to acceptself
,obj
andtype_
arguments.. TheMock
object at least uses*args, **kwargs
signatures for their callables. -
Ivo van der Wijk about 10 yearsNo, python refuses the assignment: AttributeError: 'dict' object has no attribute 'get'
-
Ivo van der Wijk about 10 years
-
Martijn Pieters about 10 years@IvovanderWijk: That'd be correct, because
dict()
objects do not support setting additional attributes (there is no.__dict__
attribute on dictionaries). -
Ivo van der Wijk about 10 yearsGood 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)
-
Dan Keder about 10 yearsIt seems that since mock-1.0.1 it isn't an issue anymore: bitbucket.org/hpk42/pytest/issue/217/…
-
Sush over 5 yearsThank you so much! This answer helped me somuch!
-
LondonRob over 2 yearsThe 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 over 2 yearsThe fact that this works does make me think that
PropertyMock
might not need to exist??