1

I can't figure out how I can mock my function in python, I've tried to look for some code, but no of it seems to work, consider this layout:

| project.py
| tests.py

project.py:

def foobar():
    return

tests.py:

import unittest
from unittest.mock import patch
import os

from project import foobar

def os_urandom_mock():
    return 'mocked'

def foobar_mock():
    return 'mocked'


class TestProject(unittest.TestCase):

    # mocked os.urandom worked well
    @patch('os.urandom', side_effect=os_urandom_mock)
    def test_os_urandom_mocked(self, os_urandom_mocked):
        self.assertEqual(os.urandom(), 'mocked')

    # but this doesn't
    @patch('project.foobar', side_effect=foobar_mock)
    def test_foobar_mocked(self, foobar_mocked):
        self.assertEqual(foobar(), 'mocked')

    # and this also doesn't work
    @patch('project.foobar')
    def test_foobar_mocked_another(self, foobar_mocked):
        foobar_mocked.return_value = 'mocked'
        self.assertEqual(foobar(), 'mocked')

    # and this also doesn't work
    def test_foobar_mocked_last_try(self):
        with patch('project.foobar') as foobar_mocked:
            foobar_mocked.return_value = 'mocked'
            self.assertEqual(foobar(), 'mocked')

unittest.main()

so, python3 tests.py:

======================================================================
FAIL: test_foobar_mocked (__main__.TestProject)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\unittest\mock.py", line 1179, in patched
    return func(*args, **keywargs)
  File "tests.py", line 24, in test_foobar_mocked
    self.assertEqual(foobar(), 'mocked')
AssertionError: None != 'mocked'

======================================================================
FAIL: test_foobar_mocked_another (__main__.TestProject)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\unittest\mock.py", line 1179, in patched
    return func(*args, **keywargs)
  File "tests.py", line 30, in test_foobar_mocked_another
    self.assertEqual(foobar(), 'mocked')
AssertionError: None != 'mocked'

======================================================================
FAIL: test_foobar_mocked_last_try (__main__.TestProject)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 35, in test_foobar_mocked_last_try
    self.assertEqual(foobar(), 'mocked')
AssertionError: None != 'mocked'

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=3)

As you can see test_os_urandom_mocked is ok, but all other tests where I've tried to mock my foobar function have failed, don't know why, can anyone please explain if it's possible to do?

Alexey
  • 1,366
  • 1
  • 13
  • 33

2 Answers2

4

You can make this work by referring to the patched function through the module:

import unittest
from unittest.mock import patch
import os
import project

def os_urandom_mock():
    return 'mocked'

def foobar_mock():
    return 'mocked'


class TestProject(unittest.TestCase):

    @patch('os.urandom', side_effect=os_urandom_mock)
    def test_os_urandom_mocked(self, os_urandom_mocked):
        self.assertEqual(os.urandom(), 'mocked')

    @patch('project.foobar', side_effect=foobar_mock)
    def test_foobar_mocked(self, foobar_mocked):
        self.assertEqual(project.foobar(), 'mocked')

    @patch('project.foobar')
    def test_foobar_mocked_another(self, foobar_mocked):
        foobar_mocked.return_value = 'mocked'
        self.assertEqual(project.foobar(), 'mocked')

    def test_foobar_mocked_last_try(self):
        with patch('project.foobar') as foobar_mocked:
            foobar_mocked.return_value = 'mocked'
            self.assertEqual(project.foobar(), 'mocked')

unittest.main()

The key thing to take away here is that when you give patch a string, it replaces the reference at that path. This behavior is demonstrated fairly well here. (Thanks to wholevinski for the link.)

With your code as it is right now, foobar in your test file refers to the original function, because you imported it before the patch was in place. By contrast, if you refer to it through the module, you'll always use the module's reference.

Alternatively, you can do what wholevinski suggested in the comments, and have your patches replace the reference in the current scope instead of in its original module:

import unittest
from unittest.mock import patch
import os
from project import foobar

def os_urandom_mock():
    return 'mocked'

def foobar_mock():
    return 'mocked'


class TestProject(unittest.TestCase):
    @patch(__name__ + '.foobar', side_effect=foobar_mock)
    def test_foobar_mocked(self, foobar_mocked):
        self.assertEqual(foobar(), 'mocked')

    @patch(__name__ + '.foobar')
    def test_foobar_mocked_another(self, foobar_mocked):
        foobar_mocked.return_value = 'mocked'
        self.assertEqual(foobar(), 'mocked')

    def test_foobar_mocked_last_try(self):
        with patch(__name__ + '.foobar') as foobar_mocked:
            foobar_mocked.return_value = 'mocked'
            self.assertEqual(foobar(), 'mocked')

unittest.main()

Note that in most cases, you'll be patching something that is being imported by the module under test, instead of calling the patched functions directly in your test. In this case, things tend to be a lot cleaner :

asdf.py:

def baz():
    return 'baz'

project.py:

from asdf import baz

def foobar():
    return baz()

tests.py:

import unittest
from unittest.mock import patch
import os
from project import foobar

def baz_mock():
    return 'mocked'

class TestProject(unittest.TestCase):
    @patch('project.baz', side_effect=baz_mock)
    def test_foobar_mocked(self, foobar_mocked):
        self.assertEqual(foobar(), 'mocked')

    @patch('project.baz')
    def test_foobar_mocked_another(self, baz_mocked):
        foobar_mocked.return_value = 'mocked'
        self.assertEqual(foobar(), 'mocked')

    def test_foobar_mocked_last_try(self):
        with patch('project.baz') as baz_mocked:
            foobar_mocked.return_value = 'mocked'
            self.assertEqual(foobar(), 'mocked')

unittest.main()
sripberger
  • 1,682
  • 1
  • 10
  • 21
  • 1
    This will work, but you don't _need_ to reference the functions through the module. This would mean you'd have to change all of your imports everywhere to be module imports to do any mocking. You need to reference it by where the object is looked up. For instance, in his original test, I'm pretty certain he could have gone with `@patch('foobar')` and it would've worked. More info: http://www.voidspace.org.uk/python/mock/patch.html#where-to-patch – wholevinski Apr 04 '18 at 13:48
  • 1
    Thanks. I wasn't aware that you could do it without referencing the module. In this case, though, you actually can't patch 'foobar' that way, but that's simply due to the way patch works. It needs a fully qualified path to the reference it is replacing. I'll go ahead and update my answer with some notes about this. – sripberger Apr 04 '18 at 14:09
  • 1
    Gah, you're right. `patch` requires a dot delimited module string. BUT, try this: `@patch(__name__ + '.foobar')` and it'll work for patching things in the module you're currently in. Cheers! – wholevinski Apr 04 '18 at 14:17
0

In fact, what I've really wanted to do is to mock post_save django signal, so this do the trick

Alexey
  • 1,366
  • 1
  • 13
  • 33