Monday, April 22, 2013

Unittesting Without Patching: A Followup

I got a couple questions about my previous post asking why I didn't show the simpler, function-based style of parameterization. This style does make unittesting possible, and with less complexity than creating a new class:

import os

def exit_with_result(function, _exit=os._exit):
    result = function()
    if result:
        _exit(0)
    else:
        _exit(1)

The problem is that when you add arguments to a function, the parameterization leaks into your public API. This means that:
  • You need to document the fact that these extra arguments (e.g. _exit in the example above) should not be used.
  • *args and **kwargs can't be used at all.
  • Changing the function signature later on can be more difficult.
  • If you have large numbers of things you need to parameterize, the function definition gets pretty long and ugly.
In the class style in contrast the public API is not affected by the need to unittest.

What's more, you will often have a group of related functions using the same modules, functions or classes. By grouping them in a class, you can implement the parameterization hook once, rather than for every function. You can see an example of this in Crochet (specifically the Eventloop class), where the parameterized reactor is used by multiple functions. If the code you need to parameterize is already a method, setting the parameters in __init__ or as a class attribute is even more attractive, requiring only minimal additional complexity.

Update: If you go with this style of parameterization, you still need to assert that in the default case it actual calls the correct function (e.g. os._exit for exit_with_result). Probably the nicest way to do so is to use inspect.getcallargs.

3 comments:

  1. *args and **kwargs is supported in python3. See http://ideone.com/ddIvdZ

    ReplyDelete
  2. Well, imagine you are programming other language that don't have optional parameter (like javascript). You are able to do that the same way on that language. Anyways, you can verify if _exit parameter is inside **kwargs and if positive use it, otherwise, use os._exit.

    ReplyDelete
  3. Checking **kwargs does seem like a good idea. I'm going to give that a try in my code.

    ReplyDelete