Real-Time Doctest Checking in Vim
Vim is a terrific editor.
The one thing that kills me, though, is “the cycle” – the cycle of going from Vim to the console to try out what I just wrote, then back to Vim back to fix what i just wrote, then back to the console to try again. Most annoyingly this cycle is usually for little things that many modern IDE’s can catch in real-time like every programming language’s version of the proverbial forgotten semicolon. In Python, for example, I often find myself forgetting colons after blocks, or somehow screwing up indentation.
Anything I can do to catch the stupid stuff early and avoid wasting precious minutes in fix-try mode is invaluable. This is where the Vim plugin Syntastic really shines. Syntastic runs any kind of lint or compiler against your code when vim writes the file. It then gives you a nice bright arrow on the left saying HEY STUPID. Which is great. You see instantly where you goofed.
For Python, the lint/error checking tool that syntastic uses by default is Flake8. Flake8 combines error and Python style checking (against PEP8) into one tool. It tells you about the errors you definitely need to fix (missing colons and what-not) but also nags you about keeping your code consistently styled based on PEP8. Together, Flake8, Syntastic, and Vim give you a pretty amazing set of tools for doing rapid Python development. My mistakes are caught instantly and fixed right away and I spend less time in the fix-try cycle between Vim and the console.
There’s one ingredient though that seems to be screaming to be added to this killer combo — integration of Python doctests. If you’re unfamiliar, doctests provide a very low barrier to entry method of developing basic tests around a Python class or function. In Python its pretty typical to develop a class and head to the interactive shell to try out your new creation. By playing around with the most obvious cases in the Python shell, you can see very quickly whether or not your code basically works. Doctests capture your experience in the shell by building some basic tests as usage examples in the class or function’s docstring:
class Adder(object): """ Doctests examples, showing usage as if Adder were used in the console: >>> a=Adder(2,3) >>> a.add() 5 >>> b=Adder(3,3) >>> b.add() 5 """ def __init__(self, x, y): self.x = x self.y = y def add(self): print self.x + self.y
In the example above, there’s 2 doctests for the Adder class. Everything after >>> is treated as if its entered into the Python shell. Resulting output (returned or in this case printed) in the Python shellwould get output on the next line. The first doctest (adding 2 and 3) will pass. The latter, however, should fail (3+3 != 6).
Doctests fit nicely with rapid prototyping and experimentation that Python is often ideal for. They can’t and shouldn’t replace hundreds of detailed unittests, but they are great when you want to get something working reasonably well fast.
What would make them even more powerful is if we could integrate them with the Vim/Syntastic/Flake8 combo. Ideally we could seamlessly integrate doctest failures with the other Syntastic errors so test failures / syntax errors / style issues get all treated equally in the Syntastic/Vim interface. In fact thats exactly what I’ve done! In my experimental Flake8 fork I’ve added a fairly rudimentary first iteration prototype of running doctests and showing their failures by outputing them in Flake8:
I’ve done this through using Python’s doctest module. Right now the code is fairly preliminary, using the doctest unittest API to run tests and then parse the output with some regexs. If you’re curious how to work with this API, you can check out this file which has the meat of the doctest running for a specified path. As this code depends on parsing output, I’m not certain how well it will work outside of the Python 2.7 line that I’m using. If I have time and this continues to seem useful, I’ll be switching to the advanced doctest API which should be more portable.
I’ve installed this first iteration locally as a fun thing to try out. Of course it seems like an awesome idea, but I’m curious to see how well it works in practice. Is the seamless integration with other Syntastic errors appropriate? Or do we need a more test-specific Vim plugin? Do testing errors need to be more verbose? Or should one really switch to a more debug-centric outside of an editor mode to get the deep information about a test failure? Would this kind of approach be appropriate for more in-depth traditional unit test frameworks? Will the tests take such a long time to run that vim’s write performance will plummet?
If you’re curious, I encourage you to install Syntastic then try out my Flake8 fork (install via distutils) and let me know what you think (and more importantly what bugs you encounter). You may find you prefer it over the vanilla version if you use a lot of doctests. You may find I’m nuts. Either way I’d love to hear from you on what you think.