RabbitFish
RabbitFish

Reputation: 1

Python unittest: Unable to mock imported functions so that conditional evaluates to False

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?

Upvotes: 0

Views: 3311

Answers (2)

RabbitFish
RabbitFish

Reputation: 1

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)

Upvotes: 0

jfaccioni
jfaccioni

Reputation: 7539

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

Upvotes: 3

Related Questions