fbpx
dirtSimple.orgwhat stands in the way, becomes the way

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 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. 🙂

Join the discussion
9 comments
  • 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.

  • 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.

  • 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.

  • 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

dirtSimple.org

Menu

Stay In Touch

Follow our feeds or subscribe to get new articles by email on these topics:

  • RSS
  • RSS
  • RSS

 

Get Unstuck, FAST

Cover photo of "A Minute To Unlimit You" by PJ Eby
Skip to toolbar