0

I'm encountering a problem with unit testing in Python. Specifically, when I try to mock a function my code imports, variables assigned to the output of that function get assigned to a MagicMock object instead of the mock-function's return_value. I've been digging through the docs for python's unittest library, but am not having any luck.

The following is the code I want to test:

from production_class import function_A, function_B, function_M

class MyClass:
    def do_something(self):
        variable = functionB()
        if variable:
            do_other_stuff()
        else:
            do_something_else

this is what I've tried:

@mock.patch(path.to.MyClass.functionB)
@mock.patch(<other dependencies in MyClass>)
def test_do_something(self, functionB_mock):
    functionB_mock.return_value = None # or False, or 'foo' or whatever.
    myClass = MyClass()
    myClass.do_something()
    self.assertTrue(else_block_was_executed)

The issue I have is that when the test gets to variable = functionB in MyClass, the variable doesn't get set to my return value; it gets set to a MagicMock object (and so the if-statement always evaluates to True). How do I mock an imported function such that when executed, variables actually get set to the return value and not the MagicMock object itself?

  • In `path.to.MyClass` you do not include `MyClass` itself, right? – MrBean Bremen Apr 10 '20 at 17:33
  • Right, I just added it for illustration – RabbitFish Apr 10 '20 at 17:46
  • Ok, maybe you use use the mock variables in the wrong order - `functionB_mock` shall be the second argument, not the first after `self`. EDIT: as can be seen in the answer below (hadn't seen it). – MrBean Bremen Apr 10 '20 at 18:08
  • To re-iterate: The code you show is fine except for the missing argument for the second patch argument (which should come first) - if that is not the problem, you are probably not showing the code related to the problem. Try to be closer to your real code in the question. – MrBean Bremen Apr 11 '20 at 16:04

2 Answers2

3

We'd have to see what import path you're actually using with path.to.MyClass.functionB. When mocking objects, you don't necessarily use the path directly to where the object is located, but the one that the intepreter sees when recursively importing modules.

For example, if your test imports MyClass from myclass.py, and that file imports functionB from production_class.py, the mock path would be myclass.functionB, instead of production_class.functionB.

Then there's the issue that you need additional mocks of MyClass.do_other_stuff and MyClass.do_something_else in to check whether MyClass called the correct downstream method, based on the return value of functionB.

Here's a working example that tests both possible return values of functionB, and whether they call the correct downstream method:

myclass.py

from production_class import functionA, functionB, functionM


class MyClass:
    def do_something(self):
        variable = functionB()
        if variable:
            self.do_other_stuff()
        else:
            self.do_something_else()

    def do_other_stuff(self):
        pass

    def do_something_else(self):
        pass

production_class.py

import random

def functionA():
    pass

def functionB():
    return random.choice([True, False])

def functionM():
    pass

test_myclass.py

import unittest
from unittest.mock import patch
from myclass import MyClass


class MyTest(unittest.TestCase):

    @patch('myclass.functionB')
    @patch('myclass.MyClass.do_something_else')
    def test_do_something_calls_do_something_else(self, do_something_else_mock, functionB_mock):
        functionB_mock.return_value = False
        instance = MyClass()
        instance.do_something()
        do_something_else_mock.assert_called()


    @patch('myclass.functionB')
    @patch('myclass.MyClass.do_other_stuff')
    def test_do_something_calls_do_other_stuff(self, do_other_stuff_mock, functionB_mock):
        functionB_mock.return_value = True
        instance = MyClass()
        instance.do_something()
        do_other_stuff_mock.assert_called()


if __name__ == '__main__':
    unittest.main()

calling python test_myclass.py results in:

..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
jfaccioni
  • 7,099
  • 1
  • 9
  • 25
  • I've already eliminated the path as an issue. When I debug my code, I see that variable is a `MagicMock` object and that it has a value: `return_value: False`. The issue is that `variable` isn't getting set to False, it's getting set to `MagicMock` To the point about mocking `do_other_stuff` and `do_something_else`: they are defined and mocked. I omitted them for brevity. – RabbitFish Apr 10 '20 at 18:03
  • Are you actually executing `functionB`? The only explanation I could think of for the behavior you just described is if the first line of `MyClass.do_something` is written as `variable = functionB` instead of `variable = functionB()`. Sadly I won't be able to help you much more than this without looking at the actual code you try to execute. – jfaccioni Apr 10 '20 at 18:14
  • Cheers. And nope, it's definitely a function call. Integration and end 2 end tests of this system pass just fine. – RabbitFish Apr 10 '20 at 22:19
0

What I wound up doing was changing the import statements in MyClass to import the object instead of the individual methods. I was then able to mock the object without any trouble.

More explicitly I changed MyClass to look like this:

import production_class as production_class

class MyClass:
    def do_something(self):
        variable = production_class.functionB()
        if variable:
            do_other_stuff()
        else:
            do_something_else

and changed my test to

@mock.patch(path.to.MyClass.production_class)
def test_do_something(self, prod_class_mock):
    prod_class_mock.functionB.return_value = None
    myClass = MyClass()
    myClass.do_something()
    self.assertTrue(else_block_was_executed)