How to properly mock private members of a class
You can access a private attribute in Python since private and protected are by convention. - What you're looking for is basically using _ClassName__private_attribute_name
since, python carries out the renaming in order to achieve the convention agreed upon.
Example (returning a MagicMock):
with mock.patch.object(Class, '_ClassName__private_attribute_name', return_value='value') as obj_mock:
pass
Example (returning a raw value):
with mock.patch.object(Class, '_ClassName__private_attribute_name', new_callable=PropertyMock) as obj_mock:
obj_mock.return_value = 'string value'
Class
is a reference to the class itself - not the instance.
Complete Example:
from unittest.mock import patch, PropertyMock
from unittest import TestCase, main
class Private:
__attr = 'hello'
class PrivateTest(TestCase):
@patch.object(Private, '_Private__attr', new_callable=PropertyMock)
def test_private_attribute_value_change_decorator_success(self, private_mock):
obj = Private()
private_mock.return_value = 'string'
self.assertEqual('string', obj._Private__attr)
def test_private_attribute_value_change_context_manager_success(self):
with patch.object(Private, '_Private__attr', new_callable=PropertyMock) as o_mock:
obj = Private()
o_mock.return_value = 'mocked value'
self.assertEqual('mocked value', obj._Private__attr)
if __name__ == '__main__':
main()
Modifications to your example:
from unittest import TestCase, mock, main
class A:
__user_id = 3
def __init__(self, user, group):
"""
Your logic is missing - obviously
:param user:
:param group:
"""
def __get_group_members(self):
"""
Your logic is missing - obviously
:return:
"""
return ['user_1', 'user_2']
def is_member_of(self, group_name):
members = self.__get_group_members(group_name)
# will return if the user is a member of the group
return self.__user_id in members
class GroupTest(TestCase):
group_data = [1, 2]
user_id = 'test_user_id'
@mock.patch.object(A, '_A__get_group_members')
@mock.patch.object(A, '_A__user_id', new_callable=mock.PropertyMock)
def test_this_is_my_first_success(self, user_id_mock: mock.PropertyMock, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
user_id_mock.return_value = 3
x = A('user_3', 'this_group')
self.assertEqual(False, x.is_member_of('test_group'))
@mock.patch.object(A, '_A__get_group_members')
@mock.patch.object(A, '_A__user_id', new_callable=mock.PropertyMock)
def test_this_is_my_first_failure(self, user_id_mock: mock.PropertyMock, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
user_id_mock.return_value = 1
x = A('user_1', 'this_group')
self.assertEqual(True, x.is_member_of('test_group'))
if __name__ == '__main__':
main()
If you know you'll mock these two attributes in all test cases you can add the decorators on the class level and expect the arguments like-wise.
In the case where the attribute is set through the __init__
or any other method, you could simply alter it as shown below.
from unittest import TestCase, mock, main
class A:
def __init__(self, user, group):
"""
Your logic is missing - obviously
:param user:
:param group:
"""
def __get_group_members(self):
"""
Your logic is missing - obviously
:return:
"""
return ['user_1', 'user_2']
def is_member_of(self, group_name):
members = self.__get_group_members(group_name)
# will return if the user is a member of the group
return self.__user_id in members
class GroupTest(TestCase):
group_data = [1, 2]
user_id = 'test_user_id'
@mock.patch.object(A, '_A__get_group_members')
def test_this_is_my_first_success(self, get_group_members_mock: mock.MagicMock):
x = A('user_3', 'this_group')
x._A__user_id = 5
get_group_members_mock.return_value = self.group_data
self.assertEqual(False, x.is_member_of('test_group'))
@mock.patch.object(A, '_A__get_group_members')
def test_this_is_my_first_failure(self, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
x = A('user_1', 'this_group')
x._A__user_id = 1
self.assertEqual(True, x.is_member_of('test_group'))
if __name__ == '__main__':
main()
d-_-b
Updated on June 17, 2022Comments
-
d-_-b almost 2 years
I am trying to write some unit tests for a method that depends on another private method. - As shown in the example below:
def is_member_of(self, group_name): members = self.__get_group_members(group_name)
The private method that I'd like to mock is
__get_group_members
; I'd also like to mock the private attribute__user_id
since it will be used in theis_member_of
function (not shown in the example above).What I have so far:
import unittest from unittest import mock class Test(unittest.TestCase): group_data = [] user_id = 'test_user_id' def mock_dependencies(self, x): x.__user_id = mock.PropertyMock(return_value=self.user_id) x.__get_group_members = mock.MagicMock(return_value=self.group_data) def first_test(self): x = A(('name', 'group')) self.mock_dependencies(x) x.is_member_of('test_group')
When I invoke
x.is_member_of()
the mocking doesn't work as anticipated. -
d-_-b about 5 yearsWhen trying to access the member, "obj.__private_attribute_name", gives
<MagicMock name='_ClassName__private_attribute_name' id='63191920'>
which is not what specified as the return value('value') for some reason. how can I fix this? -
Julian Camilleri about 5 yearsYou're trying to use
PropertyMock
- Updated answer. -
d-_-b about 5 yearsGot `<MagicMock name='_ClassName__private_attribute_name._Test__private_attribute_name' id='64741424'>' this time. Printing "obj_mock.__private_attribute_name" right after the return_value assignment
-
Julian Camilleri about 5 yearsLet me check it for you one sec.
-
d-_-b about 5 yearsThank you for your help
-
Julian Camilleri about 5 yearsIt works fine for me; let me update the answer with another example.
-
Julian Camilleri about 5 yearsThat should do the job.
-
d-_-b about 5 yearsTested both, but did not work. I guess something wrong with my setup as it works on your side...
-
Julian Camilleri about 5 yearsIf you're mocking
__get_group_members
why do you need to mock__user_id
? -
d-_-b about 5 yearsActually,
__user_id
is also used inis_member_of
and obtained from some external service. Both__user_id
and returning data from__get_group_members
are needed for the subsequent process, which is omitted in the description above. -
Julian Camilleri about 5 yearsAhh I see. - I think I understand what you're trying to do. - Let me modify the
__user_id
as well. -
nao about 4 yearsI just end up getting AttributeError: <class MyClass does not have the attribute 'MyClass__my_function'
-
nao about 4 yearsEdit: Never mind. I eneded to put MyClass__my_function. Missed the initial ''.
-
avans over 3 yearsI see that this works, but isn't it so that the solution relies upon python internal naming conventions which may be different from one python to another and maybe also differing between python versions?
-
Julian Camilleri over 3 years@pqans I don't believe that this ever changed in regards to the naming convention of how protected/private etc work in Python. - I might be wrong.
-
Julian Camilleri over 3 yearsthen again, any major version can break compatibility and would require work to fix.
-
avans over 3 yearsGenerally I would be careful about relying on such expectations. However sometimes one needs a solution - NOW - and does not care about what happens some revisions after. This needs to be considered from case to case.
-
Julian Camilleri over 3 yearsI don't agree, I don't think this is the case; what you're saying is equivalent to writing a for loop and wondering if the syntax will change. - It's a deeply implemented feature that might change in the future, anything can, but you need to be pragmatic.