如何在单元测试中使用wxpython捕获线程事件?

时间:2013-07-19 19:16:31

标签: python unit-testing wxpython

我构建了一个运行后台线程的wx python应用程序来执行一些计算。但是,我目前的实现不允许进行单元测试。

我非常密切地基于本教程中第一个示例的实现:http://wiki.wxpython.org/LongRunningTasks

下面的代码使用wx.PostEvent()frame.connect()将寄存器结果事件发送到主框架,表明计算已完成。我还展示了一个单元测试代码片段。

但是,要捕获线程结果事件,必须启动wx.App.MainLoop()。但是,我不知道如何在单元测试中模拟这种行为。

我对GUI单元测试的理解通常是手动模拟事件。但是在这种情况下,我想让我的后台线程运行。我应该修改实施吗?或者为了单元测试的目的,我可以通过其他方式存根计算线程吗?例如,我应该在单元测试代码中运行线程,然后一旦完成,调用GUI代码直接处理此事件?

import time
from threading import *
import unittest
import wx

# Button definitions
ID_START = wx.NewId()
ID_STOP = wx.NewId()

# Define notification event for thread completion
EVT_RESULT_ID = wx.NewId()

def EVT_RESULT(win, func):
    """Define Result Event."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)

class ResultEvent(wx.PyEvent):
    """Simple event to carry arbitrary result data."""
    def __init__(self, data):
        """Init Result Event."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data
        print "inside result event"

class WorkerThread(Thread):
    """Worker Thread Class."""
    def __init__(self, notify_window, delay):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self._notify_window = notify_window
        self._want_abort = 0
        self._delay = delay
        self.start()

    def run(self):
        """Run Worker Thread."""
        for i in range(self._delay):
            print "thread running..."
            time.sleep(1)
            if self._want_abort:
                # Use a result of None to acknowledge the abort (of
                # course you can use whatever you'd like or even
                # a separate event type)
                wx.PostEvent(self._notify_window, ResultEvent(None))
                return
        wx.PostEvent(self._notify_window, ResultEvent("My result"))

    def abort(self):
        """abort worker thread."""
        self._want_abort = 1

class MainFrame(wx.Frame):
    """Class MainFrame."""
    def __init__(self, parent, id):
        """Create the MainFrame."""
        wx.Frame.__init__(self, parent, id, 'Thread Test')

        # Dumb sample frame with two buttons
        wx.Button(self, ID_START, 'Start', pos=(0,0))
        wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
        self.status = wx.StaticText(self, -1, '', pos=(0,100))

        self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
        self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)

        # Set up event handler for any worker thread results
        EVT_RESULT(self,self.OnResult)

        self.worker = None
        self.thread_running = False

    def OnStart(self, event):
        """Start Computation."""
        print "OnStart"
        self.thread_running = True
        if not self.worker:
            self.status.SetLabel('Starting computation')
            self.worker = WorkerThread(self, 3)

    def OnStop(self, event):
        """Stop Computation."""
        print "OnStop"
        if self.worker:
            self.status.SetLabel('Trying to abort computation')
            self.worker.abort()
        else:
            print "no worker"

    def OnResult(self, event):
        """Show Result status."""
        # NEVER GETS INSIDE HERE!
        print "ON RESULT"
        self.thread_running = False
        if event.data is None:
            self.status.SetLabel('Computation aborted')
        else:
            self.status.SetLabel('Computation Result: %s' % event.data)
        self.worker = None

class MainApp(wx.App):
    """Class Main App."""
    def OnInit(self):
        """Init Main App."""
        self.frame = MainFrame(None, -1)
        self.frame.Show(True)
        self.SetTopWindow(self.frame)
        return True

class TestGui(unittest.TestCase):
    def setUp(self):
         print "set up"
         self.app = MainApp(0)
         self.frame = self.app.frame
         # Need MainLoop to capture ResultEvent, but how to test?
         # self.app.MainLoop()

    def tearDown(self):
        print "tear down"
        self.frame.Destroy()

    def test1(self):
        self.assertTrue(self.frame.worker is None)
        self.assertEquals(self.frame.thread_running, False)
        self.frame.OnStart(None)
        self.assertTrue(self.frame.worker is not None)
        self.assertEquals(self.frame.thread_running, True)
        while self.frame.thread_running:
            print 'waiting...'
            time.sleep(.5)
            # NEVER EXITS!
        self.assertTrue(self.frame.worker is None)

def suite():
    suite = unittest.makeSuite(TestGui, 'test')
    return suite

if __name__ == '__main__':
    unittest.main(defaultTest='suite')

1 个答案:

答案 0 :(得分:2)

将工作线程的run()方法更改为:

def run(self):
    for i in range(self._delay):
        print "thread running..."
        time.sleep(1)
        if self._want_abort:
            self.work_done(None)
            return
    self.work_done("My result")

def work_done(self, result):
    self.result = result
    wx.PostEvent(self._notify_window, ResultEvent(result))

然后在你的test1函数中用以下代码替换你的while循环:

frame.worker.join()
frame.OnResult(ResultEvent(frame.worker.result))

不需要事件循环。