farmdev

Debugging doctests interactively

Jens W. Klein has just released a pretty cool doctest debugger tool called interlude. It's designed for a situation where you are writing doctests (perhaps in the comments of your code) and you think to yourself, hmm, what happens when I run this test? Instead of the back and forth run-test-edit cycle, well, why not just drop into a doctest session from your test suite, interact with the shell until you got it right, then copy / paste the session back into your comments? This little tool is genius. And surprisingly simple: 11 lines long (3 of those are for the shell startup message).

Jens describes an installation process that involves invoking a custom doctest runner. This can introduce a bootstrapping problem, especially if you are using a doctest runner like Nose because it's hard to customize the doctest runner. Well, actually, this bootstrapping step isn't even necessary. Here's an example:

I have a doctest in a module I'm working on called Fudge, for creating mock objects (fyi, I'll be releasing this code in a day or two). Example:

class Fake(object):
    """A fake object that replaces a real one while testing."""

    # etc ...

    def has_attr(self, **attributes):
        """Sets available attributes.
            
            >>> User = Fake('User').provides('__init__').has_attr(name='Harry')
            >>> user = User()
            >>> user.name
            'Harry'
            
        """
        self._attributes.update(attributes)
        return self

I can run doctests for this with Nose like so:

$ nosetests --with-doctest --verbose
Doctest: fudge.Fake ... ok
Doctest: fudge.Fake.calls ... ok
Doctest: fudge.Fake.expects ... ok
Doctest: fudge.Fake.has_attr ... ok

Let's say I wanted to debug the doctest interactively. All I have to do is add a call to interlude.interact() from within my doctest:


    def has_attr(self, **attributes):
        """Sets available attributes.
            
            >>> User = Fake('User').provides('__init__').has_attr(name='Harry')
            >>> user = User()
            >>> user.name
            'Harry'
            >>> import interlude
            >>> interlude.interact( locals() )
        """
        self._attributes.update(attributes)
        return self

... and next time I run the tests I get dropped into a shell that contains all variables from my doctest for me to explore:

$ nosetests --with-doctest --verbose
Doctest: fudge.Fake ... ok
Doctest: fudge.Fake.calls ... ok
Doctest: fudge.Fake.expects ... ok
Doctest: fudge.Fake.has_attr ... 
=========================================
Interlude DocTest Interactive Console - (c) BlueDynamics Alliance
Note: You have the same locals available as in your test-case. 
Ctrl-D ends session and continues testing.

>>> user
fake:User
>>> user.name
'Harry'
>>> 

And of course I can simply copy / paste that session back into my comments and remove the call to interact(). Pretty nice, huh? Good work, Jens.

You could also do this with pdb but having the doctest environment makes it as simple as copy / paste when you want to put the session back in your tests.