12

(标题是:“如何为用 Python 编写的 DBUS 服务编写单元测试?”)

我已经开始使用 dbus-python 编写 DBUS 服务,但是我在为它编写测试用例时遇到了麻烦。

这是我正在尝试创建的测试示例。请注意,我在 setUp() 中放置了一个 GLib 事件循环,这就是问题所在:

import unittest

import gobject
import dbus
import dbus.service
import dbus.glib

class MyDBUSService(dbus.service.Object):
    def __init__(self):
        bus_name = dbus.service.BusName('test.helloservice', bus = dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, '/test/helloservice')

    @dbus.service.method('test.helloservice')
    def hello(self):
        return "Hello World!"


class BaseTestCase(unittest.TestCase):

    def setUp(self):
        myservice = MyDBUSService()
        loop = gobject.MainLoop()
        loop.run()
        # === Test blocks here ===

    def testHelloService(self):
        bus = dbus.SessionBus()
        helloservice = bus.get_object('test.helloservice', '/test/helloservice')
        hello = helloservice.get_dbus_method('hello', 'test.helloservice')
        assert hello() == "Hello World!"

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

我的问题是 DBUS 实现需要您启动一个事件循环,以便它可以开始调度事件。常见的方法是使用 GLib 的 gobject.MainLoop().start() (虽然我不喜欢这种方法,如果有人有更好的建议)。如果您不启动事件循环,服务仍然会阻塞,您也无法查询它。

如果我在测试中启动我的服务,事件循环会阻止测试完成。我知道该服务正在运行,因为我可以使用 qdbus 工具从外部查询该服务,但我无法在启动它的测试中自动执行此操作。

我正在考虑在测试中进行某种进程分叉来处理这个问题,但我希望有人可能有一个更简洁的解决方案,或者至少是我编写这样一个测试的一个好的起点。

4

6 回答 6

7

在 Ali A 的帖子的帮助下,我设法解决了我的问题。需要将阻塞事件循环启动到一个单独的进程中,以便它可以在不阻塞测试的情况下监听事件。

请注意我的问题标题包含一些不正确的术语,我试图编写功能测试,而不是单元测试。我意识到了这种区别,但直到后来才意识到我的错误。

我已经调整了我的问题中的示例。它与“test_pidavim.py”示例大致相似,但使用“dbus.glib”的导入来处理 glib 循环依赖项,而不是在所有 DBusGMainLoop 内容中进行编码:

import unittest

import os
import sys
import subprocess
import time

import dbus
import dbus.service
import dbus.glib
import gobject

class MyDBUSService(dbus.service.Object):

    def __init__(self):
        bus_name = dbus.service.BusName('test.helloservice', bus = dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, '/test/helloservice')

    def listen(self):
        loop = gobject.MainLoop()
        loop.run()

    @dbus.service.method('test.helloservice')
    def hello(self):
        return "Hello World!"


class BaseTestCase(unittest.TestCase):

    def setUp(self):
        env = os.environ.copy()
        self.p = subprocess.Popen(['python', './dbus_practice.py', 'server'], env=env)
        # Wait for the service to become available
        time.sleep(1)
        assert self.p.stdout == None
        assert self.p.stderr == None

    def testHelloService(self):
        bus = dbus.SessionBus()
        helloservice = bus.get_object('test.helloservice', '/test/helloservice')
        hello = helloservice.get_dbus_method('hello', 'test.helloservice')
        assert hello() == "Hello World!"

    def tearDown(self):
        # terminate() not supported in Python 2.5
        #self.p.terminate()
        os.kill(self.p.pid, 15)

if __name__ == '__main__':

    arg = ""
    if len(sys.argv) > 1:
        arg = sys.argv[1]

    if arg == "server":
        myservice = MyDBUSService()
        myservice.listen()

    else:
        unittest.main()
于 2009-02-04T18:46:09.540 回答
3

简单的解决方案:不要通过 dbus 进行单元测试。

而是编写单元测试来直接调用您的方法。这更自然地符合单元测试的性质。

您可能还需要一些自动化集成测试,通过 dbus 检查运行,但它们不需要如此完整,也不需要单独运行。您可以在单独的进程中进行设置以启动服务器的真实实例。

于 2009-02-04T10:52:45.753 回答
2

在这里我可能有点不合群,因为我不了解 python 并且只是稍微了解这个神奇的“dbus”是什么,但如果我理解正确,它需要你创建一个带有 runloops 的相当不寻常的测试环境,扩展安装/拆卸等。

您的问题的答案是使用mocking。创建一个定义接口的抽象类,然后从中构建一个对象以在您的实际代码中使用。出于测试的目的,您构建了一个模拟对象,通过相同的接口进行通信,但具有为测试目的而定义的行为。您可以使用这种方法来“模拟” dbus 对象在事件循环中运行、执行一些工作等,然后只需专注于测试您的类应该如何对该对象完成的“工作”结果做出反应。

于 2009-02-04T11:10:34.920 回答
2

您只需要确保正确处理主循环。

def refresh_ui():
    while gtk.events_pending():
       gtk.main_iteration_do(False)

这将运行 gtk 主循环,直到它处理完所有内容,而不仅仅是运行它并阻塞。

有关实践中的完整示例,对 dbus 接口进行单元测试,请访问此处: http: //pida.co.uk/trac/browser/pida/editors/vim/test_pidavim.py

于 2009-02-04T12:36:02.613 回答
2

您也可以在 setUp 方法中非常简单地在单独的线程中启动主循环。

像这样的东西:

import threading
class BaseTestCase(unittest.TestCase):
    def setUp(self):
        myservice = MyDBUSService()
        self.loop = gobject.MainLoop()
        threading.Thread(name='glib mainloop', target=self.loop.run)
    def tearDown(self):
        self.loop.quit()
于 2009-04-17T20:27:17.623 回答
0

查看python-dbusmock库。

它将丑陋的子流程逻辑隐藏在您的眼前,因此您不必在测试中担心它。

于 2013-09-30T23:13:35.360 回答