Replacing module with a stub for unit testing

3 views
Skip to first unread message

pigma...@gmail.com

unread,
May 23, 2009, 9:00:15 AM5/23/09
to
Hi,

I'm working on a unit test framework for a module. The module I'm
testing indirectly calls another module which is expensive to access
--- CDLLs whose functions access a database.

test_MyModule --->MyModule--->IntermediateModule---
>ExpensiveModule

I want to create a stub of ExpensiveModule and have that be accessed
by IntermediateModule instead of the real version

test_MyModule --->MyModule--->IntermediateModule---
>ExpensiveModuleStub

I tried the following in my unittest:

import ExpensiveModuleStub
sys.modules['ExpensiveModule'] = ExpensiveModuleStub # Doesn't
work

But, import statements in the IntermediateModule still access the real
ExpensiveModule, not the stub.

The examples I can find of creating and using Mock or Stub objects
seem to all follow a pattern where the fake objects are passed in as
arguments to the code being tested. For example, see the "Example
Usage" section here: http://python-mock.sourceforge.net. But that
doesn't work in my case as the module I'm testing doesn't directly use
the module that I want to replace.

Can anybody suggest something?

Thanks,

Scott

Steven D'Aprano

unread,
May 23, 2009, 9:25:05 AM5/23/09
to
On Sat, 23 May 2009 06:00:15 -0700, pigmartian wrote:

> Hi,
>
> I'm working on a unit test framework for a module. The module I'm
> testing indirectly calls another module which is expensive to access ---
> CDLLs whose functions access a database.

...


> The examples I can find of creating and using Mock or Stub objects seem
> to all follow a pattern where the fake objects are passed in as
> arguments to the code being tested. For example, see the "Example
> Usage" section here: http://python-mock.sourceforge.net. But that
> doesn't work in my case as the module I'm testing doesn't directly use
> the module that I want to replace.
>
> Can anybody suggest something?

Sounds like a job for monkey-patching!

Currently, you have this:

# inside test_MyModule:
import MyModule
test_code()


# inside MyModule:
import IntermediateModule


# inside IntermediateModule:
import ExpensiveModule


You want to leave MyModule and IntermediateModule as they are, but
replace ExpensiveModule with MockExpensiveModule. Try this:

# inside test_MyModule:
import MyModule
import MockExpensiveModule
MyModule.IntermediateModule.ExpensiveModule = MockExpensiveModule
test_code()

That should work, unless IntermediateModule uses "from ExpensiveModule
import functions" instead of "import ExpensiveModule". In that case, you
will have to monkey-patch each individual object rather than the entire
module:

MyModule.IntermediateModule.afunc = MockExpensiveModule.afunc
MyModule.IntermediateModule.bfunc = MockExpensiveModule.bfunc
MyModule.IntermediateModule.cfunc = MockExpensiveModule.cfunc
# etc...

--
Steven

Ben Finney

unread,
May 23, 2009, 9:27:51 AM5/23/09
to
pigma...@gmail.com writes:

>
> import ExpensiveModuleStub
> sys.modules['ExpensiveModule'] = ExpensiveModuleStub # Doesn't
> work
>
> But, import statements in the IntermediateModule still access the real
> ExpensiveModule, not the stub.
>
> The examples I can find of creating and using Mock or Stub objects
> seem to all follow a pattern where the fake objects are passed in as
> arguments to the code being tested. For example, see the "Example
> Usage" section here: http://python-mock.sourceforge.net. But that
> doesn't work in my case as the module I'm testing doesn't directly use
> the module that I want to replace.
>
> Can anybody suggest something?

The ‘MiniMock’ library <URL:http://pypi.python.org/pypi/MiniMock>
addresses this by mocking objects via namespace.

If you know the code under test will import ‘spam.eggs.beans’, import
that yourself in your unit test module, then mock the object with
‘minimock.mock('spam.eggs.beans')’. Whatever object was at that name
will be mocked (until ‘minimock.restore()’), and other code referring to
that name will get the mocked object.

--
\ “When I turned two I was really anxious, because I'd doubled my |
`\ age in a year. I thought, if this keeps up, by the time I'm six |
_o__) I'll be ninety.” —Steven Wright |
Ben Finney

A. Cavallo

unread,
May 24, 2009, 8:14:30 AM5/24/09
to pytho...@python.org
how about the old and simple:

import ExpensiveModuleStub as ExpensiveModule

On a different league you could make use of decorator and creating caching
objects but that depends entirely on the requirements (how strict your test
must be, test data sizes involved and more, much more details).

Regards,
Antonio

Steven D'Aprano

unread,
May 24, 2009, 9:30:05 AM5/24/09
to
On Sun, 24 May 2009 13:14:30 +0100, A. Cavallo wrote:

> how about the old and simple:
>
> import ExpensiveModuleStub as ExpensiveModule

No, that won't do, because for it to have the desired effort, it needs to
be inside the IntermediateModule, not the Test_Module. That means that
IntermediateModule needs to know if it is running in "test mode" or "real
mode", and that's (1) possibly impractical, and (2) not great design.

--
Steven

Message has been deleted

Fuzzyman

unread,
Jun 4, 2009, 9:52:32 AM6/4/09
to


My Mock module, and in particular the patch decorator is designed
explicitly to do this.

You need to be careful because modules and module level globals
(including things your module imports) are global state. If you change
them for the purpose of a test you must *guarantee* to restore them
after the test.

http://www.voidspace.org.uk/python/mock/
http://www.voidspace.org.uk/python/mock/patch.html

In your case if you are testing a module which imports ExpensiveModule
then by ExpensiveModule lives in the *namespace of the module under
test*. You could patch it like this:

from mock import patch
import module

@patch('module.ExpensiveModule)
def testModule(self, mockExpensiveModule):
....

Whilst the test is running 'module' has'ExpensiveModule' mocked out
(replaced) with a Mock instance. This is passed into your test so that
you can setup behaviour and make assertions about how it is used.
After the test is completed the patching is undone.

All the best,


Michael Foord
--
http://www.ironpythoninaction.com/

Reply all
Reply to author
Forward
0 new messages