Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: How to mock class attribute initializer function

I would like to mock a module-level function used to initialize a class-level (not instance) attribute. Here's a simplified example:

# a.py    
def fn(): 
    return 'asdf'

class C:
    cls_var = fn()

Here's a unittest attempting to mock a.fn():

# test_a.py 
import unittest, mock
import a

class TestStuff(unittest.TestCase):
    # we want to mock a.fn so that the class variable
    # C.cls_var gets assigned the output of our mock

    @mock.patch('a.fn', return_value='1234')
    def test_mock_fn(self, mocked_fn):
        print mocked_fn(), " -- as expected, prints '1234'"
        self.assertEqual('1234', a.C.cls_var) # fails! C.cls_var is 'asdf'

I believe the problem is where to patch but I've tried both variations on import with no luck. I've even tried moving the import statement into test_mock_fn() so that the mocked a.fn() would "exist" before a.C comes into scope - nope, still fails.

Any insight would be greatly appreciated!

like image 545
Steven Colby Avatar asked Feb 21 '16 01:02

Steven Colby


People also ask

What is Side_effect in mock Python?

side_effect: side_effect allows to perform side effects, including raising an exception when a mock is called. patch(): The patch() decorator/ context manager makes it easy to mock classes or objects in a module under test.

Can you mock a class Python?

unittest.mock is a library for testing in Python. It allows you to replace parts of your system under test with mock objects and make assertions about how they have been used. unittest.mock provides a core Mock class removing the need to create a host of stubs throughout your test suite.

How do you mock an entire class in Python?

To mock an entire class you'll need to set the return_value to be a new instance of the class. @mock.


1 Answers

What is actually happening here is that when you actually import your module, fn() would have already executed. So, the mock comes in after you have already evaluated the method that is being stored in your class attribute.

So, by the time you try to mock the method it is too late for the testing you are trying to do.

You can even see this happening if you simply add a print statement in your method:

def fn():
    print("I have run")
    return "asdf"

In your test module, when you import a and simply run without even running your test and you will see I have run will come up in your console output without running anything explicitly from your a module.

So, there are two approaches you can take here. Either you can use the PropertyMock to mock out the class attribute to what you are expecting it to store, like this:

@mock.patch('a.C.cls_var', new_callable=PropertyMock)
def test_mock_fn(self, mocked_p):
    mocked_p.return_value = '1234'

    self.assertEqual('1234', a.C.cls_var)

Now, you have to be also aware, that by doing this, you are still actually running fn, but with this mocking, you are now holding '1234' in cls_var with the PropertyMock you have set up.

The following suggestion (probably less ideal since it requires a design change) would be requiring revising why you are using a class attribute. Because if you actually set that class attribute as an instance attribute, that way when you create an instance of C then your method will execute, which at that point it will use your mock.

So, your class looks like:

class C:
    def __init__(self):
        self.var = fn()

and your test would look like:

@mock.patch('a.fn', return_value='1234')
def test_mock_fn(self, mocked_p):
    self.assertEqual('1234', a.C().var)
like image 132
idjaw Avatar answered Oct 18 '22 09:10

idjaw