Bar
Bar

Reputation: 2826

Mocking instantiated object vs. class in Python

Could someone help me understand the difference between unitest.patch instantianed objects and classes? I'm trying to mock calls to sagemaker.Session.describe_training_job. I have a function that calls my_session = sagemaker.Session() then calls my_session.describe_training_job() later:

#my_module.py
import sagemaker


def do_something_with_session(local_session, job):
    return local_session.describe_training_job(job)

def my_function(args):
    my_session = sagemaker.Session()

    description = do_something_with_session(my_session, args.job)

If I use the test below, the my_session object is a MagicMock as expected, but does not have the describe_training_job method:

@patch("sagemaker.Session")
def test_class(self, sagemaker_session):
    sagemaker_session.describe_training_job = MagicMock(return_value=self.mock_return_value)
    my_module.my_function(args=self.mock_args)

If I instead do this:

@patch("sagemaker.Session")
def test_class(self, sagemaker_session):
    # Need to instantiate an object for this to work
    session_object = sagemaker_session()
    session_object.describe_training_job = MagicMock(return_value=self.mock_return_value)
    my_module.my_function(self.mock_args)

Then the test works as expected: the my_session object is a MagicMock with a describe_training_job method that always returns the value I set it to.

Could someone help understand what the behavior is here? What I've noticed when is that the name parameter of the MagicMock is Session when I try to use the class, but Session() when I do instantiate the object. Not sure how that affects binding.

Upvotes: 1

Views: 1737

Answers (1)

MrBean Bremen
MrBean Bremen

Reputation: 16855

A mock is a placeholder object, it does not have the mocked methods (though it may know which methods can be called if using autospec). Every method call on the mock that you do not explicitly provide just returns another mock object.

If you mock a class, and that class is instantiated in the tested code, the resulting instance will be another mock. The instantation is just a __call__ for the mock, which returns another mock as every call does (and it always returns the same mock for the same call). Similar to the result of a function call, this mock can be accessed via return_value.

If you set an attribute on the class mock like in your first example, it is only bound to that class mock, not to the instance mock, so this allows to mock only class methods. The main point to understand here is that the class mock and the instance mock are not related like the class and the instance they are mocking. Both have the same Mock type, and the instance mock doesn't know the class mock that has created it.

This means that instance method attributes have always to be set on the instance mock, as you have done in your second example. Using return_value is eqivalent to that, so you could also write:

    sagemaker_session.return_value.describe_training_job = MagicMock(return_value=self.mock_return_value)

Upvotes: 3

Related Questions