Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python, mocking and wrapping methods without instantating objects

I want to mock a method of a class and use wraps, so that it is actually called, but I can inspect the arguments passed to it. I have seen at several places (here for example) that the usual way to do that is as follows (adapted to show my point):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    spud = Potato()

    @patch.object(Potato, 'foo', wraps=spud.foo)
    def test_something(self, mock):
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

However, this instantiates the class Potato, in order to bind the mock to the instance method spud.foo.

What I need is to mock the method foo in all instances of Potato, and wrap them around the original methods. I.e, I need the following:

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

This of course doesn't work. I get the error:

TypeError: foo() missing 1 required positional argument: 'self'

It works however if wraps is not used, so the problem is not in the mock itself, but in the way it calls the wrapped function. For example, this works (but of course I had to "fake" the returned value, because now Potato.foo is never actually run):

from unittest import TestCase
from unittest.mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', return_value=42)#, wraps=Potato.foo)
    def test_something(self, mock):
        self.spud = Potato()
        forty_two = self.spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

This works, but it does not run the original function, which I need to run because the return value is used elsewhere (and I cannot fake it from the test).

Can it be done?

Note The actual reason behind my needs is that I'm testing a rest api with webtest. From the tests I perform some wsgi requests to some paths, and my framework instantiates some classes and uses their methods to fulfill the request. I want to capture the parameters sent to those methods to do some asserts about them in my tests.

like image 919
JLDiaz Avatar asked Jun 27 '17 10:06

JLDiaz


People also ask

What is the difference between MagicMock and mock?

So what is the difference between them? MagicMock is a subclass of Mock . It contains all magic methods pre-created and ready to use (e.g. __str__ , __len__ , etc.). Therefore, you should use MagicMock when you need magic methods, and Mock if you don't need them.

How do you use mock method in Python?

How do we mock in Python? Mocking in Python is done by using patch to hijack an API function or object creation call. When patch intercepts a call, it returns a MagicMock object by default. By setting properties on the MagicMock object, you can mock the API call to return any value you want or raise an Exception .

When should you not use a mock?

Only use a mock (or test double) “when testing things that cross the dependency inversion boundaries of the system” (per Bob Martin). If I truly need a test double, I go to the highest level in the class hierarchy diagram above that will get the job done. In other words, don't use a mock if a spy will do.

What are mock objects in Python?

A mock object substitutes and imitates a real object within a testing environment. It is a versatile and powerful tool for improving the quality of your tests. One reason to use Python mock objects is to control your code's behavior during testing.


1 Answers

In short, you can't do this using Mock instances alone.

patch.object creates Mock's for the specified instance (Potato), i.e. it replaces Potato.foo with a single Mock the moment it is called. Therefore, there is no way to pass instances to the Mock as the mock is created before any instances are. To my knowledge getting instance information to the Mock at runtime is also very difficult.

To illustrate:

from unittest.mock import MagicMock

class MyMock(MagicMock):
    def __init__(self, *a, **kw):
        super(MyMock, self).__init__(*a, **kw)
        print('Created Mock instance a={}, kw={}'.format(a,kw))

with patch.object(Potato, 'foo', new_callable=MyMock, wrap=Potato.foo):
    print('no instances created')
    spud = Potato()
    print('instance created')

The output is:

Created Mock instance a=(), kw={'name': 'foo', 'wrap': <function Potato.foo at 0x7f5d9bfddea0>}
no instances created
instance created

I would suggest monkey-patching your class in order to add the Mock to the correct location.

from unittest.mock import MagicMock

class PotatoTest(TestCase):
    def test_something(self):

        old_foo = Potato.foo
        try:
            mock = MagicMock(wraps=Potato.foo, return_value=42)
            Potato.foo = lambda *a,**kw: mock(*a, **kw)

            self.spud = Potato()
            forty_two = self.spud.foo(n=40)
            mock.assert_called_once_with(self.spud, n=40) # Now needs self instance
            self.assertEqual(forty_two, 42)
        finally:
            Potato.foo = old_foo

Note that you using called_with is problematic as you are calling your functions with an instance.

like image 190
Dave Evans Avatar answered Oct 15 '22 22:10

Dave Evans