Monday, December 06, 2004

Mocking wxPython

I've seen the question asked from time to time: how do you unit test a wxPython application?

Well, I don't have a silver bullet, but it seems to me that the new SWIG wrapping of recent wxPython versions allows a new possibility: mocking the entire wxPython library by replacing its .pyd/.so modules with Python objects that replace their behavior. For example, I was able to import the wx package from wxPython, without using any of its C++ extensions, by using this horrible hack:
from types import ModuleType

class Dummy(ModuleType):
def __getattr__(self,name):
if name.startswith('__'):
raise AttributeError, name
if name.endswith('VERSION') or name.startswith('VERSION'):
import wx.__version__
return getattr(sys.modules['wx.__version__'],name)
return lambda *__args,**__kw: None

def _wxPySetDictionary(self,d):
d['Platform'] = '__WXMSW__'
d['__wxPyPtrTypeMap'] = {}

_core_ = Dummy()
_core_.cvar = Dummy()
_gdi_ = Dummy()
_gdi_.cvar = Dummy()
_windows_ = Dummy()
_windows_.cvar = Dummy()
_controls_ = Dummy()
_controls_.cvar = Dummy()

import sys
sys.modules['wx._core_'] = _core_
sys.modules['wx._gdi_'] = _gdi_
sys.modules['wx._windows_'] = _windows_
sys.modules['wx._controls_'] = _controls_
sys.modules['wx._misc_'] = _controls_

import wx
Gross, eh?

Of course, this is far from being enough to provide an adequate mock-up of wxPython, since it just makes a lot of symbols be function objects returning None, which for example will cause errors when you try to OR together some of the symbols that should be bitflags! To be completely effective, there are a lot of symbols that really need individual tweaking here, and perhaps some implementation of stub behaviors, like actually saving width/height settings so they can be read back. The top-level wx namespace alone defines over 3000 symbols, so this is potentially quite a lot of tweaking!

On the other hand, this took only 10 or 15 minutes to throw together, and can be gradually enhanced to add the features needed for a specific application's unit tests. I'm already thinking about how I could add a way to change the mocked behaviors on the fly... for example, to switch between apparent platforms, so you could test that your application properly detects various platforms and behaves accordingly.

The exciting thing about it, though -- at least from my point of view -- is the potential for doing "headless" GUI unit testing. Naturally, it's not a substitute for acceptance testing with the real GUI, but it sure would be nice for fast developer unit testing. In a way, I'm almost surprised there isn't a "no-GUI" feature like this already built in to wxWidgets itself. That would certainly be easier to use than my crazy hack. (Naturally, it's possible that there is such a thing and I just haven't figured out how to use it yet.)

Of course, my hack wouldn't even be possible without wxPython's recent major overhaul of its namespaces, and its Python binding strategies in general. wxPython now uses new-style classes with nice generated Python methods throughout, and its approach of wrapping the C++ modules in Python modules is what made this hack so easy. Its method wrapping will also come in handy for unit testing, because my 'peak.util.unittrace' module will be able to record that a given set of wxPython API methods were called. (Because 'unittrace' uses the pure-Python profiler hook, it can only track calls to Python functions or methods, not C ones.)

All in all, wxPython seems to have grown up quite a bit since the last time I played with it (a few years ago). I guess maybe you could say that now it's mature enough to be able to accept a little good-natured mockery. :)