【问题标题】:How to write a functional test for a DBUS service written in Python?如何为用 Python 编写的 DBUS 服务编写功能测试?
【发布时间】:2010-10-05 09:06:35
【问题描述】:

(标题是:“如何为用 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 工具从外部查询该服务,但我无法在启动它的测试中自动执行此操作。

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

【问题讨论】:

    标签: python unit-testing dbus


    【解决方案1】:

    在 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()
    

    【讨论】:

    • 谢谢 - 非常方便。虽然我建议使用__file__ 使对自己的子流程调用更加清晰和便携:self.p = subprocess.Popen(['python', __file__, 'server'], env=env)
    【解决方案2】:

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

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

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

    【讨论】:

    • 我认为你是对的。回想起来,只要直接测试服务方法,我认为无论如何测试 dbus 接口不会有什么收获。
    • 这很容易,并不难。你必须这样做。永远不要仅仅因为有人告诉你就跳过单元测试,人们给出这样的建议真的很令人沮丧。是的,你应该在没有 dbus 的情况下测试你的东西,是的,你应该使用 dbus 来测试它。
    • 但是如果它们依赖于 dbus 的启动和工作,它们就不是单元测试。因此,为功能编写单元测试,以及使用 dbus 进行更复杂设置(多进程等)的集成测试。
    • Douglas:我不同意,尽管这是一个语义问题。我正在使用更大的“单位”。
    • 根据定义将 dbus 置于其间使其超出单元测试范围。
    【解决方案3】:

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

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

    【讨论】:

    • 我喜欢这个概念。我想如果我的 DBUS 接口类开始包含一些逻辑,我会使用 mocking 来模拟行为。
    【解决方案4】:

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

    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

    【讨论】:

    • 您引用的测试似乎将 DBUS 服务器分叉为一个单独的进程(在 Vim 内部)。我的事件循环没问题,但例子证明整个过程不能在测试中完成。感谢您指出。
    【解决方案5】:

    您也可以在您的 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()
    

    【讨论】:

      【解决方案6】:

      查看python-dbusmock 库。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-02
        • 2020-11-28
        • 1970-01-01
        • 2017-04-15
        • 1970-01-01
        • 2014-02-26
        相关资源
        最近更新 更多