Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Mock a module without importing it or needing it to exist

I am starting to use a python mock library for my testing. I want to mock a module that is imported within the namespace of the module under test without actually importing it or requiring that it exist first (i.e. throwing an ImportError).

Suppose the following code exists:

foo.py

 import helpers
 def foo_func():
    return helpers.helper_func()

The goal is to test foo_func() when 'helpers.py' does not exist anywhere, and if it does exist, act as if it doesn't.

First try, using the super cool @patch decorator:

from mock import patch, sentinel
import foo
@patch("foo.helpers")
def foo_test(mock):
    mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

This works if the "helpers" module can be imported. If it doesn't exist, I get an ImportError.

Next attempt with patch, sans decorator:

from mock import patch, sentinel, Mock
import foo
helpers_mock = patch("foo.helpers")
helpers_mock.start()

def foo_test():
    helpers_mock.helper_func = Mock('helper_func')
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

Again, this doesn't work if "helpers" is missing... and, if it exists, the assertion fails. Not really sure why that happens.

Third attempt, current solution:

import sys
helpers_mock = Mock(name="helpers_mock", spec=['helper_func'])
helpers_mock.__name__ = 'helpers'
sys.modules['helpers'] = helpers_mock
import foo
def foo_test():
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

This test passes regardless of whether or not "helpers.py" exists.

Is this the best way to accomplish this goal? Does the mocking library I am using provide an alternative to this? What other ways can I do this?

like image 754
Kyle Gibson Avatar asked Mar 01 '11 06:03

Kyle Gibson


2 Answers

You're kind of missing the point of what a Mock is. You're supposed to build them when you want an object with a particular interface, regardless of how it's implemented.

What you're doing is trying to re-implement python's module system, plus it's a pretty horrible abuse of global variables to boot.

Instead of making foo a module, make a Foo class, and pass in the helpers in the constructor.

class Foo(object):
    def __init__(self, helpers):
        self.helpers = helpers

# then, instead of import foo:
foo = Foo(mock_helpers)

Even if the real "helpers" is actually going to be a module, there is no reason you need to be messing with sys.modules and setting it up via import in your tests.

And if foo has to be a module, that's fine too, but you do it like this:

# foo.py
class Foo(object):
    pass # same code as before, plus foo_func

try:
   import whatever
   _singleton = Foo(whatever)
except ImportError:
   _singleton = Foo(something_else)

def foo_func():
   return _singleton.foo_func()

Large chunks of the standard library work this way. It's pretty much the standard for defining singleton-like modules.

like image 192
tangentstorm Avatar answered Sep 25 '22 07:09

tangentstorm


I had a similar problem where the helpers library couldn't be loaded as it needed special hardware. Rather than make radical changes to the code that you want to test, an alternative is to insert a "fake" directory into sys.path as suggested by how to add a package to sys path for testing

import os, sys
fake_dir = os.path.join(os.path.dirname(__file__), 'fake')
assert(os.path.exists(fake_dir))
sys.path.insert(0, fake_dir)
import foo
from unittest.mock import sentinel
def foo_test():
    foo.helpers.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

where fake is structured as:

.
├── fake/
│   └── helpers/
│       └── __init__.py
├── foo.py
└── helpers/

and __init__.py has

from unittest.mock import Mock
helper_func = Mock()
like image 31
James Brusey Avatar answered Sep 24 '22 07:09

James Brusey