1

Consider a module with a 'public' function which looks something like this:

def func(arg):
    val = _generate_something(arg)
    _do_something(val)

As you can see this is a 'void function'. Its core logic is in _generate_something.

I want to write unit tests for this function. I can think of several approaches to do so. I would like to know what the best one is, both in terms of general programming practices and best practice in Python specifically.

Essentialy this question is about the Pythonic way of mocking a private function (money patching? Injecting the function? Injecting an object? Etc.)


Option A

In the test monkeypatch _do_something (which is a 'private' function of the module), so I can check it receives a good value.


Option B

Inject _do_something as an argument to func, like so:

def func(arg, do_something=None):
    do_something = do_something or _do_something  # in production use default
    val = _generate_something(arg)
    do_something(val)

Then in my do_something mock I can check I received the correct value.


Option C

Have func be a method of a class, and have that class take do_something as an argument to __init__:

class Something(object):
    def __init__(self, do_something=None):
        self._do_something = do_something or _production_default

    def func(self, arg):
        val = self._generate_something(arg)
        self.do_something(val)

    # ... omitted for brevity ...

Option D

Like Option B or C, but do_something is injected as a 'full' object, not a bare function. For example:

def func(arg, something_doer=None):
    something_doer = something_doer or _something_doer # in production use default
    val = _generate_something(arg)
    something_doer.do_something(val)

As I said, I would like to know what the best option is, also with regard to 'Pythonicness'. For example, is it 'acceptable' in Python to mock bare functions (rather than objects which contain them)?

  • Do you know the mock library? – RemcoGerlich Nov 8 '16 at 8:59
2

I would recommend to use the most simple and shortest approach, which is in Python probably option A, monkey patching. Everything else makes the code more complicated, with no substantial benefit. The only thing I would change here is to make this solution more explicit (see below).

Converting your module to a class (option C) should be motivated by the fact you need a class instead of a module, not by the fact you want to mock a single function. This option is preferable in languages like Java or C#, where there is no distinction between a "module" and a "class" in a way which is comparable to Python, and where a function like func would probably "live" inside a class at first hand.

Replacing the method do_something by an object with a method (option D) should not be done for the same reason - use an object instead of a standalone function when a standalone function is "not enough", for the sheer purpose of mocking it brings no advantage.

So this leaves you with option B or A. However, I would not pollute the parameter list of func for the purpose of mocking, nor would I stuff mocking code into a function func just for the purpose of mocking if there is simpler alternative. Better keep the "official" API as as well the functional code as clean as possible. Imagine what happens if you want to mock more "private" methods instead of just one, and if you want to test 5 public methods of the module - the API will contain 90% parameters only used for testing, which decreases the readability by a magnitude.

If you have concerns about the monkey patching approach because it changes something private, you can add a "public" (or "internal") patching function to the module, which allows the manipulation to be done through an "official" function:

def setup_mock_for_do_something(do_something):
    _do_something = do_something

This makes the possibility of "monkey patching" in this module more explicit, by simultanously avoiding unneccessary structural changes to the whole code.

So why is this all more "Pythonic"? Looking at "The Zen of Python", we see the following principles applied here

  • Explicit is better than implicit.
  • Simple is better than complex.
  • Readability counts.
  • If the implementation is easy to explain, it may be a good idea.
1

Note: OP has significantly edited and changed the question, so this answer is a bit obsolete, but leaving for now for history. Original answer follows:

Assuming that _generate_something() and _do_something() work, and are properly unit tested individually, a good argument can be made not to bother testing func() at all. IMO, it's well past the point of diminishing returns in unit-testing. YMMV. (Note: If there were many lines of code between the two lines in your example then a test makes sense)

For example, your Options B-D, as I understand them, are just a complicated way to test the results of _generate_something(), which you could just test directly. Now, maybe they make sense in the overall design of your program, but adding that amount of complexity just for unit tests is a mistake.

  • Thanks for your answer. Two questions: first, _generate_something is a 'private' function, an 'implementation detail', while func is a public function of the module. Is it good practice and common to just test _generate_something directly (i.e. test private methods)? Secondly, I understand what you mean about func not being wotthwhile to test anyway. But still, I'd like an answer to the question of what is the good and Pythonicbway to mock _do_something. Which of the four approaches is the best? – Aviv Cohn Nov 8 '16 at 1:57
  • Private does not necessarily mean "implementation detail". A private method may (or may not) be worthy of testing - but sounds like it is for your case. As for most "Pythonic", my python experience is very limited so others should answer that. – user949300 Nov 8 '16 at 4:55
  • @AvivCohn: well if it's an implementation detail that you don't want to test directly, you probably shouldn't mock it when testing func. – RemcoGerlich Nov 8 '16 at 9:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.