Phil Gyford
Phil Gyford

Reputation: 14594

Mock a class and a class method in python unit tests

I'm using python's unittest.mock to do some testing in a Django app. I want to check that a class is called, and that a method on its instance is also called.

For example, given this simplified example code:

# In project/app.py
def do_something():
    obj = MyClass(name='bob')
    return obj.my_method(num=10)

And this test to check what's happening:

# In tests/test_stuff.py
@patch('project.app.MyClass')
def test_it(self, my_class):
    do_something()
    my_class.assert_called_once_with(name='bob')
    my_class.my_method.assert_called_once_with(num=10)

The test successfully says that my_class is called, but says my_class.my_method isn't called. I know I'm missing something - mocking a method on the mocked class? - but I'm not sure what or how to make it work.

Upvotes: 3

Views: 3086

Answers (3)

idjaw
idjaw

Reputation: 26578

A small refactoring suggestion for your unittests to help with other instance methods you might come across in your tests. Instead of mocking your class in each method, you can set this all up in the setUp method. That way, with the class mocked out and creating a mock object from that class, you can now simply use that object as many times as you want, testing all the methods in your class.

To help illustrate this, I put together the following example. Comments in-line:

class MyTest(unittest.TestCase):

    def setUp(self):
        # patch the class
        self.patcher = patch('your_module.MyClass')
        self.my_class = self.patcher.start()

        # create your mock object
        self.mock_stuff_obj = Mock()
        # When your real class is called, return value will be the mock_obj
        self.my_class.return_value = self.mock_stuff_obj

    def test_it(self):
        do_something()

        # assert your stuff here
        self.my_class.assert_called_once_with(name='bob')
        self.mock_stuff_obj.my_method.assert_called_once_with(num=10)

    # stop the patcher in the tearDown
    def tearDown(self):
        self.patcher.stop()

To provide some insight on how this is put together, inside the setUp method we will provide functionality to apply the patch across multiple methods as explained in the docs here.

The patching is done in these two lines:

    # patch the class
    self.patcher = patch('your_module.MyClass')
    self.my_class = self.patcher.start()

Finally, the mock object is created here:

    # create your mock object
    self.mock_stuff_obj = Mock()
    self.my_class.return_value = self.mock_stuff_obj

Now, all your test methods can simply use self.my_class and self.mock_stuff_obj in all your calls.

Upvotes: 3

Anthony Kong
Anthony Kong

Reputation: 40624

This line

 my_class.my_method.assert_called_once_with(num=10)

will work if my_method is a class method.

Is it the case?

Otherwise, if my_method is just an normal instance method, then you will need to refactor the function do_something to get hold of the instance variable obj

e.g.

def do_something():
    obj = MyClass(name='bob')
    return obj, obj.my_method(num=10)

# In tests/test_stuff.py
@patch('project.app.MyClass')
def test_it(self, my_class):
    obj, _ = do_something()
    my_class.assert_called_once_with(name='bob')
    obj.my_method.assert_called_once_with(num=10)

Upvotes: 1

Jared
Jared

Reputation: 26397

Your second mock assertion needs to test that you are calling my_method on the instance, not on the class itself.

Call the mock object like this,

my_class().my_method.assert_called_once_with(num=10)
        ^^

Upvotes: 3

Related Questions