简体   繁体   English

如何测试 PyQt/PySide 信号?

[英]How to test PyQt/PySide signals?

I'm trying to test a class that takes "events" from another library and re-dispatches them as Signals/pyqtSignals.我正在尝试测试一个 class,它从另一个库中获取“事件”并将它们重新分配为 Signals/pyqtSignals。

I'm trying to test it by:我正在尝试通过以下方式对其进行测试:

  1. Connecting a handler.连接处理程序。
  2. Performing some action that should trigger the event (and thus, the Signal/pyqtSignal to be emitted, which should call the handler).执行一些应该触发事件的操作(因此,要发出的 Signal/pyqtSignal 应该调用处理程序)。
  3. Checking to see if the handler was called.检查是否调用了处理程序。

But the handler never gets called.但是处理程序永远不会被调用。 Or at least it's not getting called by the time I check for it.或者至少在我检查它时它没有被调用。 If I run the lines of the test function one-by-one in a console, the test is successful.如果我在控制台中逐行运行测试 function 的行,则测试成功。

I think the problem is that the Signals aren't getting processed until after the test function completes, but if that's the case, how would I go about testing them?我认为问题在于信号在测试 function 完成后才得到处理,但如果是这样的话,我 go 如何测试它们?

Here is an example of how I'm trying to test:这是我如何尝试测试的示例:

from mock import Mock


def test_object():
    obj = MyObject()
    handler = Mock()

    # This connects handler to a Signal/pyqtSignal
    obj.connect('event_type', handler)

    # This should trigger that signal
    obj.do_some_stuff()

    # This fails (call_count is 0)
    assert handler.call_count = 1

    # This will also fail (call_count is 0)
    QtCore.QCoreApplication.instance().processEvents()
    assert handler.call_count = 1

You might need to run a local event loop when waiting for the signals to be dispatched. 等待信号分派时,可能需要运行本地事件循环。

Since you seem to be using pytest (if not, you should consider to!), I'd recommend the pytest-qt plugin. 由于您似乎正在使用pytest(如果没有,应该考虑使用!),所以我建议使用pytest-qt插件。 With it, you could write your test like this: 有了它,您可以像这样编写测试:

def test_object(qtbot):
    obj = MyObject()

    with qtbot.waitSignal(obj.event_type, raising=True):
        obj.do_some_stuff()

The objects you are connecting might be garbage-collected before the signal is received because they are created in the scope of the test_object function and might not have time to complete. 您正在连接的对象可能在收到信号之前被垃圾回收,因为它们是在test_object函数范围内创建的,可能没有时间完成。

EDIT: example code: 编辑:示例代码:

from PySide import QtCore, QtGui

class MyObject(QtCore.QObject):
    fired = QtCore.Signal()
    def __init__(self, parent=None):
        super(MyObject, self).__init__(parent)

    def do_some_stuff(self):
        self.fired.emit()

class Mock(object):
    def __init__(self, parent=None):
        self.call_count = 0

    def signalReceived(self):
        self.call_count+=1
        print "Signal received, call count: %d" % self.call_count

def test_object():
    obj = MyObject()
    handler = Mock()
    obj.fired.connect(handler.signalReceived)
    obj.do_some_stuff()
    assert handler.call_count == 1
    QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)


test_object()

The application is not even instantiated when you call do_some_stuff(). 当您调用do_some_stuff()时,该应用程序甚至都没有实例化。 If there is no app, then it will be created when you first call QCoreApplication.instance() 如果没有应用程序,则将在您首次调用QCoreApplication.instance()时创建。

So all you have to do is insert a line at the beginning of the function to instantiate the application. 因此,您要做的就是在函数的开头插入一行以实例化应用程序。 You wont event need the process event method, becouse Signals are not events. 您不会使用事件方法来处理事件,因为信号不是事件。 Events are gui events. 事件是gui事件。 Signals at least by default are called immediatelly. 至少默认情况下会立即调用信号。

this is from Qt documentation: 这来自Qt文档:

When a signal is emitted, the slots connected to it are usually executed immediately, just like a normal function call. 发出信号后,与其连接的插槽通常会立即执行,就像正常的函数调用一样。 When this happens, the signals and slots mechanism is totally independent of any GUI event loop. 发生这种情况时,信号和时隙机制完全独立于任何GUI事件循环。 Execution of the code following the emit statement will occur once all slots have returned. 一旦所有插槽都返回,将执行emit语句之后的代码。 The situation is slightly different when using queued connections; 使用排队连接时,情况略有不同。 in such a case, the code following the emit keyword will continue immediately, and the slots will be executed later. 在这种情况下,发出关键字之后的代码将立即继续,并且稍后将执行插槽。

http://doc.qt.io/qt-5/signalsandslots.html http://doc.qt.io/qt-5/signalsandslots.html

def test_object():
    app = QtCore.QCoreApplication.instance() # isntantiate the app
    obj = MyObject()
    handler = Mock()

    # This connects handler to a Signal/pyqtSignal
    obj.connect('event_type', handler)

    # This should trigger that signal
    obj.do_some_stuff()

    # This fails (call_count is 0)
    assert handler.call_count = 1

    # This will also fail (call_count is 0)
    assert handler.call_count = 1

Why don't you use python's unittest module? 为什么不使用python的unittest模块? I just wanted to test PySide signals as well and subclassed the TestCase class to support that. 我只是想测试PySide信号,并将其子类化为TestCase类来支持它。 You can subclass this and use the assertSignalReceived method, to test if a signal was called. 您可以将此子类化,并使用assertSignalReceived方法来测试是否调用了信号。 It also tests for arguments. 它还测试参数。 Let me know it it works for you! 让我知道它对您有用! I haven't used it much yet. 我还没用过。

example: 例:

class TestMySignals(UsesQSignals):
    def test_my_signal(self):
        with self.assertSignalReceived(my_signal, expected_args):
            my_signal.emit(args)

UsesQSignal class, has a little context manager UsesQSignal类,有一个小的上下文管理器

class SignalReceiver:
    def __init__(self, test, signal, *args):
        self.test = test
        self.signal = signal
        self.called = False
        self.expected_args = args

    def slot(self, *args):
        self.actual_args = args
        self.called = True

    def __enter__(self):
        self.signal.connect(self.slot)

    def __exit__(self, e, msg, traceback):
        if e: 
            raise e, msg
        self.test.assertTrue(self.called, "Signal not called!")
        self.test.assertEqual(self.expected_args, self.actual_args, """Signal arguments don't match!
            actual:   {}
            expected: {}""".format(self.actual_args, self.expected_args))


class UsesQSignals(unittest.TestCase):
    def setUp(self):
        from PySide.QtGui import QApplication

        '''Creates the QApplication instance'''
        _instance = QApplication.instance()
        if not _instance:
            _instance = QApplication([])
        self.app = _instance

    def tearDown(self):
        '''Deletes the reference owned by self'''
        del self.app

    def assertSignalReceived(self, signal, args):
        return SignalReceiver(self, signal, args)

I ran into the same issue, but am not using pytest and so cannot use the recommended plugin.我遇到了同样的问题,但我没有使用 pytest,因此无法使用推荐的插件。

My hacky solution which worked better than expected was to set up the widget under test how I want it, actually launch the app, then have a special option in the widget to call QApplication.quit() when the method is done being tested.我的 hacky 解决方案比预期效果更好,它是按照我想要的方式设置被测小部件,实际启动应用程序,然后在小部件中有一个特殊选项,以便在方法完成测试时调用QApplication.quit() So far I have unit tests where I call a function using pyqt5 signals 10s of times using this method without issue.到目前为止,我进行了单元测试,其中我使用 pyqt5 信号调用 function 10 次,使用此方法没有问题。

For me this is testing a data loading class which uses workers and signals.对我来说,这是在测试使用工作人员和信号的数据加载 class。 Here is a cut down version of the code.这是代码的简化版本。 First the class under test is below.首先下面是被测的class。

from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtCore import QTimer


class LoadWidget(QWidget):
    def __init__(self, *args, test_mode=False, **kwargs):
        super(LoadWidget, self).__init__(*args, **kwargs)
        self.obj = ''

        if test_mode:
            self.timer = QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(lambda: QApplication.quit())
            self.timer.start(100)

    def load(fname):
        # The following two lines happens in a worker and a signal calls  
        # another class method on completion to deposit the loaded data  
        # in self.obj, but for brevity I will just write it out here
        with open(fname) as f:
            self.obj = f.read()

Then the test itself.然后是测试本身。

import unittest
import random
import string
from PyQt5.QtWidgets import QApplication


ctx = QApplication([])


class LoadTests(unittest.TestCase):
    def test_load_file(self):
        test_dat = ''.join(random.choice(string.ascii_lowercase) for _ in range(10))
        with open("test.txt", 'w') as f:
            f.write(test_dat)
        w = LoadWidget(test_mode=True)
        w.load("test.txt")
        ctx.exec_()
        self.assertEqual(test_dat, w.obj)

Edit: Just after getting this working in my example, I ran into another problem where the event loop isn't getting called because the load function doesn't end until calling application quit.编辑:在我的例子中得到这个工作后,我遇到了另一个问题,事件循环没有被调用,因为负载 function 直到调用应用程序退出才结束。 This affected some of my tests.这影响了我的一些测试。 This can be mitigated by adding the quit call to a timer and I'm updating my code to reflect that.这可以通过将 quit 调用添加到计时器来缓解,我正在更新我的代码以反映这一点。

Further Edit: The timer must be called in single-shot mode otherwise it will keep calling across further tests and cause difficult to diagnose issues.进一步编辑:必须在单次模式下调用计时器,否则它将继续调用进一步的测试并导致难以诊断的问题。 Since it is a timer it can be set in the class constructor which allows it to be shared across multiple methods.因为它是一个计时器,所以它可以在 class 构造函数中设置,这允许它在多个方法之间共享。

Final Note: Having the QApplication be created in setUp caused weird crashes when I started running many tests in a suite and spawning many applications.最后注意:当我开始在套件中运行许多测试并生成许多应用程序时,在 setUp 中创建 QApplication 会导致奇怪的崩溃。 This was solved by moving it into the main body of the file and just calling the global version.这是通过将其移动到文件的主体并仅调用全局版本来解决的。

Final Final Note Using a timer to kill the application doesn't even require modification of the widget being tested. Final 最后说明使用计时器终止应用程序甚至不需要修改被测试的小部件。 You can simply create a function to stick a timer to it before launching the application in your test.在测试中启动应用程序之前,您可以简单地创建一个 function 以在其上粘贴一个计时器。 This simplifies things and allows for functions involving signals to be run more than once on a single instance by launching the app multiple times.这简化了事情,并允许通过多次启动应用程序在单个实例上多次运行涉及信号的功能。

import unittest
import random
import string
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer


ctx = QApplication([])


def add_timer(w):
    w.timer = QTimer()
    w.timer.setSingleShot(True)
    w.timer.timeout.connect(lambda: QApplication.quit())
    w.timer.start(100)


class LoadTests(unittest.TestCase):
    def test_load_file(self):
        test_dat = ''.join(random.choice(string.ascii_lowercase) for _ in range(10))
        with open("test.txt", 'w') as f:
            f.write(test_dat)
        w = LoadWidget(test_mode=True)

        # Load the file
        w.load("test.txt")
        add_timer(w)
        ctx.exec_()

        # Demonstrate how one would load multiple times
        w.load("test.txt")
        add_timer(w)
        ctx.exec_()

        self.assertEqual(test_dat, w.obj)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM