5

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!

Steven Colby
  • 53
  • 1
  • 5
  • have you tried to change the import to use from statement? from a import fn – Rainer Feb 21 '16 at 01:32
  • Hi Ranier - yep, tried that; no luck. (I should have been clearer when I mentioned '...both variations on import...'. The Python docs on mock give examples of using `import a` and `from a import SomeClass`. I tried both flavors) – Steven Colby Feb 21 '16 at 04:12

2 Answers2

5

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)
idjaw
  • 25,487
  • 7
  • 64
  • 83
  • Hi idjaw - hey, I like the `PropertyMock` idea! I will try that. Letting `fn()` run is less than ideal, but it's worth a try. Regarding your second idea, believe me I'd love to refactor this code. But of course the example I gave was an abstraction of the real problem which involves SQLAlchemy. SQLAlchemy makes extensive use of class attributes and there's little I can do to change it. Thanks! – Steven Colby Feb 21 '16 at 04:21
0

Even @idjaw's answer is correct and explain correctly what happen I think the most concise, simple and direct way to do it is to patch directly a.C.cls_var attribute by a value instead a mock.

@mock.patch('a.C.cls_var', '1234')

This is enough for everything you need: use PropertyMock is useful just when you must replace a property that should behave like a property.

For every details about why your approach doesn't work take a look to @idjaw's answer.

Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76