Of course you can unit test apps built with wxPython! To start with, Python provides a unittest module for unit testing. This facility is often referred to as PyUnit. This is very useful for testing non-GUI portions of your wxPython program, e.g. testing API methods, etc.
Why Unit Test? (For those not yet convinced...!)
Many people new to unit testing think it's a tool for making sure a function or method works as expected, which is only half the story. Since most people do run tests on their functions, they see unit tests as almost redundant - that is to say, "why go through all this work writing unit tests when I'll know immediately when the function breaks?". The perceived benefit is that you could test a large number of possible inputs, but in many cases, even with unit tests you can't do that, so why bother if the bug you're trying to avoid will show up anyway?
However, perhaps the most important benefit of unit tests is not that it ensures that your code works - it's that it ensures that it stays working, that is, that a future change doesn't cause a regression. Nearly every function has bugs, and this way, once you fix a bug, you write a test so that if the bug ever reappears, you know immediately, and can fix it before it gets into users' hands. It's true you often can't write tests against all possible valid and invalid input data, but when you do get a bug reported, you can then write a unit test against the particular condition that caused the bug, and from that point, you can ensure you'll never have a release that fails under that condition again. As these conditions grow, so do your unit tests, meaning that there are less and less untested conditions you need to worry about. Thus you spend less and less time tracking down regressions in old code, and more and more time working on new code. (Which, of course, is the fun stuff!) You probably will also be less concerned about refactoring and improving code when you know that mistakes will be caught.
Adding unit tests to your code
Thanks to the dynamic nature of Python, it's not only easy to add unit tests to your code, but you can even use a utility called figleaf to let you know which code paths are, and are not, covered by your unit tests. A tutorial on general unit testing is beyond the scope of this page, but a template example on how to unit test a wxPython application can be found here:
If you download those three files into the same directory, and run testExample.py, you should see the test passes. Now, if you install figleaf and then run testExample.py through figleaf and generate the HTML report (see this page for instructions), you can see not only what percentage of the code has been tested, but also every line of code that wasn't tested by the unit tests. Using these tools, you can keep eliminating untested code paths until you can say with confidence that a vast majority of your code paths are automatically tested on each build, which would be a pretty impressive feat. Not that this is a competition, but if it were, you'd have a major lead on most other OSS projects out there!
Still, with GUI programs particularly, you can't achieve near-complete code coverage just by using the methods shown in the example above. Which leads us to...
Unit tests and GUI controls
Testing that the GUI portions of an application behave properly is a bit more tricky because GUIs are based around user events, and so we need to find a way to simulate these. There are a couple different ways to approach testing of GUI apps:
- To simulate user interaction (e.g. user clicking a button) and test event handlers, use an automated GUI testing tool
- To test that wx or a wx app responds to a particular event handler, simulate the event by firing your own wx event on the window, or just fire the event handler function with a None event.
- To test your app's interaction with a GUI class, write a stub implementation of the GUI class that returns valid (and if applicable non-valid) results.
- To test setting and retrieving control state and properties, use traditional unit tests. (see above example)
These options will be discussed more below.
Automated GUI Testing tools
This offers the most "authentic" testing because it truly simulates a user interacting with the application. The downside is that often these tools are commercial, and even if they aren't, they are often platform-specific. However, there are some free, Python-based options available on Windows which you can find here. On Unix, there's Dogtail. An example of using Dogtail to test a GUI application can be found here http://www.redhat.com/magazine/020jun06/features/dogtail/. (If anyone knows of any other alternatives, particularly a free Mac GUI tester, or has sample test code, please list them here!)
Simulating wx events
Another option is to call the handler functions yourself. For example, if you're just interested in testing how the application reacts when a button is pressed, you just need to fire the button event handler yourself in order to simulate a button press. (Be sure, if you access the event object, that you pass in an object with any properties the handler uses.) If you don't care about testing the actual button press (because really, a button press should turn into a function call to do something, and the tests can just call the function directly), then there's no reason why you can't still build the UI and make sure its state is valid!
Writing a stub implementation
In this approach, you stub out portions of the program you don't want to test yourself; that is, any code you "expect" to return the correct value. Examples would include Python library code or wxPython code, which you assume is being tested by people in the respective projects. Then, you don't have to worry about a GUI application stealing the focus, wanting input, or not closing a frame/dialog when those things aren't important to your tests.
Details of how to use this approach can be found at: wxStubmaker
By the way, wxStubmaker replaces NDTestmaker, which is no longer supported.
Another tool you can use for this is called PythonMock (you will need to subscribe to the Extreme Programming group on Yahoo to get the file). It's a simple concept - using Mock Objects to enable easy unit testing. Python Mock implements a class that can be passed to functions under test instead of a class used during standard operations. The Mock class records all function calls and the parameters passed to the functions.
I've had some success unit testing a no-ui version of my wxPython programs by using Python Mock (mentioned above) and a hack from DirtSimple. You need to simulate the button clicks, or other UI events yourself by calling your methods, but it least you are able to test it.
- I have created various manipulator classes to simulate user events. For example, here's a manipulator for a button:
class WxButtonManip(WxWidgetManip): def __init__(self, button): WxWidgetManip.__init__(self, button) def click(self): clickEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId()) self._widget.ProcessEvent(clickEvent)
Those work pretty well for unit testing individual dialogs in isolation from one another. But I still haven't figured out how to do global acceptance testing of wxPython apps. The problem is that as soon as the app starts a modal dialog, control does not return until the modal dialog has exited, at which time it's too late for the testing script to enter data into it.
Has anyone figured out a solution to this? thx
alain dot desilets at nrc-cnrc dot gc dot ca
I have figured out how to test a modal dialog using wx.CallAfter. Here is the full code:
import unittest import wx class MyDialog(wx.Dialog): def __init__(self, parent): wx.Dialog.__init__(self, parent, -1, 'Test') wx.Button(self, wx.ID_OK) class TestMyDialog(unittest.TestCase): def setUp(self): self.app = wx.App() self.frame = wx.Frame(None) self.frame.Show() def tearDown(self): wx.CallAfter(self.app.Exit) self.app.MainLoop() def testDialog(self): def clickOK(): clickEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, wx.ID_OK) self.dlg.ProcessEvent(clickEvent) wx.CallAfter(clickOK) self.ShowDialog() def ShowDialog(self): self.dlg = MyDialog(self.frame) self.dlg.ShowModal() self.dlg.Destroy() if __name__ == '__main__': unittest.main()