1

The Production file (production_file.py) is:

class MyError(Exception):
    pass

class MyClass:
    def __init__(self):
        self.value = None

    def set_value(self, value):
        self.value = value

    def foo(self):
        raise RuntimeError("error!")


class Caller:
    def bar(self, smth):
        obj = MyClass()
        obj.set_value(smth)

        try:
            obj.foo()
        except MyError:
            pass

        obj.set_value("str2")
        obj.foo()

Test file (test.py):

import unittest

from unittest.mock import patch
from unittest.mock import call
from production_file import MyClass, Caller

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            my_class_mock_obj = MyClassMock.return_value
            my_class_mock_obj.foo.side_effect = [MyError("msg"), "text"]

            caller = Caller()
            caller.bar("str1")

            calls = [call("str1"), call("str2")]

            my_class_mock_obj.set_value.assert_has_calls(calls)

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

This above works. But if I move the production classes (MyError, MyClass, Caller) into the test file, and update patch to:

with patch('test.MyClass', autospec=MyClass) as MyClassMock:

then the instance method "foo" is no longer mocked.

Does anybody have any idea why that is?

I have also experienced a similar problem with some more complex code, where the production code is in my_package/src/production_file.py while the test is in my_package/tests/test_file.py. Python yields no error for the path, the path is correct, but still the mock doesn't work.

Feoggou
  • 149
  • 3
  • 11

1 Answers1

2

If you are running test.py as __main__ then it is not test.MyClass it would be __main__.MyClass, or in both cases __name__+".MyClass".

I was able to determine that the class used and the class patched were different by adding a print statement:

class Caller:
    def bar(self, smth):
        print(MyClass) #lets see what we are actually making an instance of...
        obj = MyClass()
        ...

When the patch is applied to the class that this is using you would see something like this:

<MagicMock name='MyClass' spec='MyClass' id='4387629656'>

But when the class in moved into test.py you will see something like:

<class '__main__.MyClass'>

Which indicates:

  1. There was no patching applied to MyClass (at least the one that is used for the test.)
  2. The name of the class that needs to be patched is __main__.MyClass

However It is quite likely that your "more... complicated situation" is not working because of a setup like this:

from production_file import MyClass

class MyError(Exception):
    pass


class Caller:
    def bar(self, smth):
        print(MyClass)
        obj = MyClass()
        ...

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            ...

In this case production_file.MyClass is being patched and MyClass is being imported from production_file so the correct class is being patched but still the output is:

<class 'production_file.MyClass'>

This is because the Class was directly imported to the local namespace, so when the patch is applied to the production_file the local namespace is still unaffected, we can check that the patch was actually applied with:

...
def bar(self, smth):
    print(MyClass)
    from production_file import MyClass as pf_MyClass
    print(pf_MyClass)
...


#output:
<class 'production_file.MyClass'>
<MagicMock name='MyClass' spec='MyClass' id='4387847136'>

If this is the case you just need to import the module, not the class directly. Then once the patch is applied you will be using it right from the file:

import production_file

...
class Caller:
    def bar(self, smth):
        print(production_file.MyClass)
        obj = production_file.MyClass()
        ...

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            ...
Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
  • Interesting... but why does not python yield an error, if the path is wrong? – Feoggou Apr 03 '16 at 01:31
  • because nothing is going wrong, it is just patching the wrong place, if you have a file `test.py` and in it you ` import test` you end up with two versions of the same file loaded with different names. if you added `if __name__ == "__main__": from test import MyClass ...` etc it would work as expected since then it is patching the class you are using for the test. – Tadhg McDonald-Jensen Apr 03 '16 at 01:43
  • I also have a problem with a more... complicated situation, where I have my_package/src/my_file.py and my_package/tests/test.py. In this case, one of my tests uses patch("src.my_file.MyClass"), but still the mock doesn't work. Is there a way for me to debug / log what the test is actually patching? – Feoggou Apr 03 '16 at 08:25
  • does my edit provide a more comprehensive explication / analysis? I've never actually used unittest so I was mostly fumbling in the dark until I felt something I recognized, in this case it was inconsistent namespaces which I have seen before with [pickling classes from `__main__`](http://stackoverflow.com/questions/27732354/unable-to-load-files-using-pickle-and-multipile-modules). – Tadhg McDonald-Jensen Apr 03 '16 at 15:20
  • Thanks! that was the fix for my problem. Interestingly enough, at print(MyClass), it yielded the full path (starting with the package name). But the problem was fixed - and the mock now got to be printed out there. – Feoggou Apr 09 '16 at 21:02
  • "I've never actually used unittest" - so what framework do you use? unittest is the first and only one I used for python :) – Feoggou Apr 09 '16 at 21:05
  • never used unittest or any equivalent framework, I've never really worked on any project large enough that it benefited from running test cases etc. – Tadhg McDonald-Jensen Apr 09 '16 at 22:17