7

I have a module with a function (call it a()) that calls another function defined in the same module (call it __b()). __b() is a function which speaks to a website via urllib2 and gets some data back. Now I'm trying to test a(), but of course would rather not have my unit tests speak to the public internet. Thus, I'm thinking if I can monkey patch __b() with a function which returns canned data, then I can write the tests for a().

To be more concrete, my module looks kinda like:

def a():
    return __b("someval")

def __b(args):
    return something_complex_with_args

So now I want to test a(), but I need to monkey patch out __b. The problem is that A) the vast majority of information on monkey patching applies to methods of a class, not to functions in a module, and B) the function I want to monkey patch is private. I am willing to change __b to be non-private if it makes the process more feasible, but would rather not.

Suggestions?

Edit: as it stands the test class looks like:

from unittest import TestCase

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule._b = newfn

class TestMyModule(TestCase):
    def test_basic(self):
        print(mymodule.a('somearg'))

And when I run this, I see the output if the monkey patching had not been done at all, rather than seeing {'a': 'b'} get printed out.

Adam Parkin
  • 17,891
  • 17
  • 66
  • 87
  • 4
    In general, if *anything* outside the module needs to access a function (including tests), you probably shouldn't be using `__name` format. Use `_name` format instead to signify that it's an internal function while still making it possible for the things that do need to access it to get at it. – Amber Dec 26 '12 at 21:48
  • 2
    Name mangling (`__x`) is not the same as *private* and you unless you know exactly what it is, you probably shouldn't be using it. Python does not have data hiding. – Gareth Latty Dec 26 '12 at 21:51
  • @Lattyware: I'm aware that it's not the same thing as private, but it's as close as it gets. Why is there the concern about having it named with double underscore? – Adam Parkin Dec 27 '12 at 03:45
  • @AdamParkin Because the dobule underscore provides name mangling - the name gets changed (look up to docs for why and exactly what happens). You don't need to make things private in Python. Your problem is a very clear example of why it's a bad idea. Python isn't Java or C++, and writing it as though it is will result in bad code. – Gareth Latty Dec 27 '12 at 10:50
  • 1
    By "to docs" do you mean (http://docs.python.org/2/tutorial/classes.html#tut-private)? So then I still fail to see why it's generally bad. Perhaps move this to chat since it's not particularly relevant to my question? – Adam Parkin Dec 28 '12 at 03:39

3 Answers3

4

I can't seem to reproduce your issue (I've tweaked your example a bit, since it doesn't run at all as-is). Did you just mistype something (like mymodule._b instead of mymodule.__b)?

mymodule.py:

def a(x):
    return __b("someval")

def __b(args):
    return "complex_thingy: {}".format(args)

mytest.py:

from unittest import TestCase

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule.__b = newfn

class TestMyModule(TestCase):
    def test_basic(self):
        print(mymodule.a('somearg'))

Output:

C:\TEMP>python -m unittest mytest
{'a': 'b'}
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

C:\TEMP>

Seems to work fine.


Or outside of unittest:

mytest2.py:

import mymodule

def newfn(args):
    return {"a" : "b"}

mymodule.__b = newfn

print(mymodule.a('somearg'))

Output:

C:\TEMP>python mytest2.py
{'a': 'b'}

C:\TEMP>
Gerrat
  • 28,863
  • 9
  • 73
  • 101
2

If your module was named 'foo', then the following should work.

import foo

def patched_version():
    return 'Hello'

foo.__b = patched_version

print (foo.a())

where foo.py is

def a():
    return __b()

def __b():
    return 'Goodbye'
jeffknupp
  • 5,966
  • 3
  • 28
  • 29
  • 1
    Nope, original version of __b() is still called (regardless of whether it's `__b` or `_b`). – Adam Parkin Dec 27 '12 at 03:49
  • 1
    Looking into this a bit more, your answer works, but when I change the print to be inside a test method (like in my question), the original version of `__b` is called. – Adam Parkin Dec 27 '12 at 03:59
  • It's because unittest executes very differently than 'normal' Python. Try putting the `foo.__b = patched_version` in a `setUp` function for your test. If you're using something other than `unittest`, YMMV. – jeffknupp Dec 27 '12 at 22:49
  • I am using `unittest` and moving it into `setUp` did not resolve the problem. – Adam Parkin Dec 28 '12 at 03:41
0

I was facing the same problem, but finally got to the solution. The problem was when using the monkey patch in the unittest.TestCase class. Here's the solution that works just fine:

If you are using Python 2, you'll need to install the "mock" library (http://www.voidspace.org.uk/python/mock/) using easy_install or some other way. This library is already bundled with Python 3.

Here's what the code looks like:

from mock import patch

    class TestMyModule(TestCase):
        def test_basic(self):
            with patch('mymodule._b') as mock:
                mock.return_value={"a" : "b"} # put here what you want the mock function to return. You can make multiple tests varying these values.
                #keep the indentation. Determines the scope for the patch.
                print(mymodule.a('somearg'))

Although this way apparently seems a bit less convenient compared to making a mock function where we could mimic the actual sub-function, _b(), having logic to return different values based on different arguments, but then we unnecessarily add more chances of error. In this approach we just hard-code what we want our mocked function to return and test the actual function we set out to test, that was a().

Afzal Naushahi
  • 175
  • 1
  • 4
  • Nope, still didn't work for me. I get: `AttributeError: does not have the attribute '_b'` Adding the "private" function to __init__.py made the error go away, but then the patching had no effect (original version was still being called). – Adam Parkin Jan 27 '13 at 14:51