简体   繁体   中英

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.

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)
        ^^

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.

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

eg

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM