smart
smart

Reputation: 2065

Python: mocks in unittests

I have situation similar to:

class BaseClient(object):
    def __init__(self, api_key):
        self.api_key = api_key
        # Doing some staff.

class ConcreteClient(BaseClient):
    def get_some_basic_data(self):
        # Doing something.

    def calculate(self):
        # some staff here
        self.get_some_basic_data(param)
        # some calculations

Then I want to test calculate function using mocking of get_some_basic_data function.

I'm doing something like this:

import unittest
from my_module import ConcreteClient

def my_fake_data(param):
    return [{"key1": "val1"}, {"key2": "val2"}]

class ConcreteClientTest(unittest.TestCase):
    def setUp(self):
        self.client = Mock(ConcreteClient)

    def test_calculate(self):

        patch.object(ConcreteClient, 'get_some_basic_data',
                     return_value=my_fake_data).start()
        result = self.client.calculate(42)

But it doesn't work as I expect.. As I thought, self.get_some_basic_data(param) returns my list from my_fake_data function, but it looks like it's still an Mock object, which is not expected for me.

What is wrong here?

Upvotes: 1

Views: 52

Answers (1)

idjaw
idjaw

Reputation: 26578

There are two main problems that you are facing here. The primary issue that is raising the current problem you are experiencing is because of how you are actually mocking. Now, since you are actually patching the object for ConcreteClient, you want to make sure that you are still using the real ConcreteClient but mocking the attributes of the instance that you want to mock when testing. You can actually see this illustration in the documentation. Unfortunately there is no explicit anchor for the exact line, but if you follow this link:

https://docs.python.org/3/library/unittest.mock-examples.html

The section that states:

Where you use patch() to create a mock for you, you can get a reference to the mock using the “as” form of the with statement:

The code in reference is:

class ProductionClass:
    def method(self):
        pass

with patch.object(ProductionClass, 'method') as mock_method:
    mock_method.return_value = None
    real = ProductionClass()
    real.method(1, 2, 3)

mock_method.assert_called_with(1, 2, 3)

The critical item to notice here is how the everything is being called. Notice that the real instance of the class is created. In your example, when you are doing this:

self.client = Mock(ConcreteClient)

You are creating a Mock object that is specced on ConcreteClient. So, ultimately this is just a Mock object that holds the attributes for your ConcreteClient. You will not actually be holding the real instance of ConcreteClient.

To solve this problem. simply create a real instance after you patch your object. Also, to make your life easier so you don't have to manually start/stop your patch.object, use the context manager, it will save you a lot of hassle.

Finally, your second problem, is your return_value. Your return_value is actually returning the uncalled my_fake_data function. You actually want the data itself, so it needs to be the return of that function. You could just put the data itself as your return_value.

With these two corrections in mind, your test should now just look like this:

class ConcreteClientTest(unittest.TestCase):

    def test_calculate(self):

        with patch.object(ConcreteClient, 'get_some_basic_data',
                     return_value=[{"key1": "val1"}, {"key2": "val2"}]):

            concrete_client = ConcreteClient(Mock())
            result = concrete_client.calculate()

        self.assertEqual(
            result,
            [{"key1": "val1"}, {"key2": "val2"}]
        )

I took the liberty of actually returning the result of get_some_basic_data in calculate just to have something to compare to. I'm not sure what your real code looks like. But, ultimately, the structure of your test in how you should be doing this, is illustrated above.

Upvotes: 1

Related Questions