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 2.5.2.9, 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. 🙂
It took me a while, but I just got that pun. Ouch.
maybe it won’t be such an ugly hack if you try to use the new wxversion module. Maybe use a version of wxpython tailored for unittesting. Something like wx-2.5.3-unittest 🙂 and in the unittests just say:
import wxversion
wxversion.select(‘2.5.3-unittest’)
then carry on with the testing.
So, does such a version exist? Or are you just saying that that’s a cleaner way to implement the replacement technique?
I am do wxPython unittesting for my projects. Nothing fancy except the small patch (clearing one global variable) needed because I use wx.CallAfter extensively. I use “test-controllable” MainLoop that exits periodically (via ExitMainLoop). Fairly receint versions (2.5.x series) works fine with this technique.
How is this comparable to
NDtestmaker?
NDTestmaker rewrites *your* code to call other functions, and it still depends on booting wxPython. For best results, unit tests should be run against unmodified code, and with a minimum of external interactions. Also, if you want to unit test your application’s reactions to different platforms, it might be easier to mock wxPython instead of using NDTestmaker.
Of course, if you need something that can be used right away, NDTestmaker is the natural choice. However, with current versions of wxPython, NDTestMaker could run much more simply by replacing wx._core_.*ShowModal and the like, rather than by rewriting the calling programs.
This comment has been removed by a blog administrator.
This comment has been removed by a blog administrator.
It works!
Here’s what I did for a small wxPython program.
I used mock.py by Dave Kirkby which I is availabe at yahoo groups, extremeprogramming under the files section.
It ain’t pretty but I was able to test what I needed to.
Ooops: looks like blogger.com is going to swallow all the indentation.
——- wxMock ——–
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(“dummy”)
_core_.cvar = Dummy(“dummy”)
_gdi_ = Dummy(“dummy”)
_gdi_.cvar = Dummy(“dummy”)
_windows_ = Dummy(“dummy”)
_windows_.cvar = Dummy(“dummy”)
_controls_ = Dummy(“dummy”)
_controls_.cvar = Dummy(“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_
sys.modules[‘wx._html’] = _controls_
sys.modules[‘wx._grid’] = _controls_
import wx
import wx.html
import wx.grid
from mock import Mock
def makeAssociation(strClass, strModule = ‘wx’):
if ‘wxMock’ in sys.modules:
setattr(sys.modules[strModule], strClass, getattr(sys.modules[‘wxMock’], ‘My’ + strClass))
else:
setattr(sys.modules[strModule], strClass, getattr(sys.modules[‘__main__’], ‘My’ + strClass))
class wxMock(Mock):
def __init__(self, *args, **kwargs):
Mock.__init__(self)
#################################################
class MyFrame(wxMock):
pass
makeAssociation(‘Frame’)
class MyIcon(wxMock):
pass
makeAssociation(‘Icon’)
class MyMenuBar(wxMock):
pass
makeAssociation(‘MenuBar’)
class MyMenu(wxMock):
pass
makeAssociation(‘Menu’)
class MyAcceleratorTable(wxMock):
pass
makeAssociation(‘AcceleratorTable’)
class MyStaticText(wxMock):
pass
makeAssociation(‘StaticText’)
class MyTextCtrl(Mock):
def __init__(self, *args, **kwargs):
self.strValue = ”
Mock.__init__(self)
def GetValue(self):
return self.strValue
def SetValue(self, str):
self.strValue = str
makeAssociation(‘TextCtrl’)
class MyNotebook(wxMock):
pass
makeAssociation(‘Notebook’)
class MyBoxSizer(wxMock):
pass
makeAssociation(‘BoxSizer’)
class MyRadioButton(wxMock):
pass
makeAssociation(‘RadioButton’)
class MyPanel(wxMock):
pass
makeAssociation(‘Panel’)
class MyButton(wxMock):
pass
makeAssociation(‘Button’)
class MyHtmlEasyPrinting(wxMock):
pass
makeAssociation(‘HtmlEasyPrinting’, ‘wx.html’)
class MyHtmlWindow(wxMock):
pass
makeAssociation(‘HtmlWindow’, ‘wx.html’)
class MyGrid(Mock):
def __init__(self, *args, **kwargs):
self.data = [[]]
self.nCols = 0
self.nCurRow = 0
Mock.__init__(self)
def CreateGrid(self, numRows, numCols):
self.data = []
self.nCols = numCols
for nRow in range(numRows):
self.AppendRows(1)
def SetCellValue(self, row, col, s):
self.data[row][col] = s
def GetNumberRows(self):
return len(self.data)
def DeleteRows(self, pos = 0, numRows = 1, updateLabels = True):
print “Todo”
def AppendRows(self, numRows = 1, updateLabels = True):
for nRow in range(numRows):
self.data.append([ [] for nCol in range(self.nCols)])
return True
def GetGridCursorRow(self):
return self.nCurRow
def MoveCursorDown(self, expandSelection):
self.nCurRow += 1
if self.nCurRow >= self.GetNumberRows():
self.nCurRow -= 1
makeAssociation(‘Grid’, ‘wx.grid’)
wx.WXK_NUMPAD0 = 326
wx.WXK_NUMPAD9 = 335