How to properly mock private members of a class

10,740

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()
Share:
10,740
d-_-b
Author by

d-_-b

Updated on June 17, 2022

Comments

  • d-_-b
    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 the is_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
    d-_-b about 5 years
    When 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
    Julian Camilleri about 5 years
    You're trying to use PropertyMock - Updated answer.
  • d-_-b
    d-_-b about 5 years
    Got `<MagicMock name='_ClassName__private_attribute_name._Test__private_attr‌​ibute_name' id='64741424'>' this time. Printing "obj_mock.__private_attribute_name" right after the return_value assignment
  • Julian Camilleri
    Julian Camilleri about 5 years
    Let me check it for you one sec.
  • d-_-b
    d-_-b about 5 years
    Thank you for your help
  • Julian Camilleri
    Julian Camilleri about 5 years
    It works fine for me; let me update the answer with another example.
  • Julian Camilleri
    Julian Camilleri about 5 years
    That should do the job.
  • d-_-b
    d-_-b about 5 years
    Tested both, but did not work. I guess something wrong with my setup as it works on your side...
  • Julian Camilleri
    Julian Camilleri about 5 years
    If you're mocking __get_group_members why do you need to mock __user_id ?
  • d-_-b
    d-_-b about 5 years
    Actually, __user_id is also used in is_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
    Julian Camilleri about 5 years
    Ahh I see. - I think I understand what you're trying to do. - Let me modify the __user_id as well.
  • nao
    nao about 4 years
    I just end up getting AttributeError: <class MyClass does not have the attribute 'MyClass__my_function'
  • nao
    nao about 4 years
    Edit: Never mind. I eneded to put MyClass__my_function. Missed the initial ''.
  • avans
    avans over 3 years
    I 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
    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
    Julian Camilleri over 3 years
    then again, any major version can break compatibility and would require work to fix.
  • avans
    avans over 3 years
    Generally 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
    Julian Camilleri over 3 years
    I 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.