views:

1374

answers:

5

(Title was: "How to write a unit test for a DBUS service written in Python?")

I've started to write a DBUS service using dbus-python, but I'm having trouble writing a test case for it.

Here is an example of the test I am trying to create. Notice that I have put a GLib event loop in the setUp(), this is where the problem hits:

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

My problem is that the DBUS implementation requires you to start an event loop so that it can start dispatching events. The common approach is to use GLib's gobject.MainLoop().start() (although I'm not married to this approach, if someone has a better suggestion). If you don't start an event loop, the service still blocks, and you also cannot query it.

If I start my service in the test, the event loop blocks the test from completing. I know the service is working because I can query the service externally using the qdbus tool, but I can't automate this inside the test that starts it.

I'm considering doing some kind of process forking inside the test to handle this, but I was hoping someone might have a neater solution, or at least a good starting place for how I would write a test like this.

+2  A: 

Simple solution: don't unit test through dbus.

Instead write your unit tests to call your methods directly. That fits in more naturally with the nature of unit tests.

You might also want some automated integration tests, that check running through dbus, but they don't need to be so complete, nor run in isolation. You can have setup that starts a real instance of your server, in a separate process.

Douglas Leeder
I think you're right. On reflection, I don't think I'd get much from testing the dbus interface anyway, as long as the service methods are tested directly.
seanhodges
This is easy, not hard. And you MUST do it. Don't ever skimp on unit tests just because someone tells you to, and it is really depressing that people give this kind of advice. Yes you should test your stuff without dbus, and yes you should test it with dbus.
Ali A
But they're not unit tests if they rely on dbus being up and working. Therefore write unit tests for the functionality, and integration tests that have more complicated setup (multi-process etc) with dbus.
Douglas Leeder
Douglas: I have to disagree, although it is a semantic issue. I am taking bigger "units".
Ali A
+1  A: 

I might be a bit out of my league here, since I don't know python and only somewhat understand what this magical "dbus" is, but if I understand correctly, it requires you to create a rather unusual testing environment with runloops, extended setup/teardown, and so on.

The answer to your problem is to use mocking. Create an abstract class which defines your interface, and then build an object from that to use in your actual code. For the purposes of testing, you build a mock object communicates through that same interface, but has behavior which you would define for the purposes of testing. You can use this approach to "simulate" the dbus object running through an event loop, doing some work, etc., and then simply concentrate on testing how your class ought to react to the result of the "work" done by that object.

Nik Reiman
I like this concept. I think if my DBUS interface class starts to contain some logic, I'll use mocking to simulate the behaviour.
seanhodges
+2  A: 

You just need to make sure you are handling your main loop properly.

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

This will run the gtk main loop until it has finished processing everything, rather than just run it and block.

For a complete example of it in practise, unit testing a dbus interface, go here: http://pida.co.uk/trac/browser/pida/editors/vim/test_pidavim.py

Ali A
The test you referenced appears to be forking the DBUS server as a separate process (inside Vim). My event loop was OK, but the example proves that the whole process cannot be done within the test. Thanks for pointing it out.
seanhodges
+1  A: 

With some help from Ali A's post, I have managed to solve my problem. The blocking event loop needed to be launched into a separate process, so that it can listen for events without blocking the test.

Please be aware my question title contained some incorrect terminology, I was trying to write a functional test, as opposed to a unit test. I was aware of the distinction, but didn't realise my mistake until later.

I've adjusted the example in my question. It loosely resembles the "test_pidavim.py" example, but uses an import for "dbus.glib" to handle the glib loop dependencies instead of coding in all the DBusGMainLoop stuff:

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()
seanhodges
+1  A: 

You could also start the mainloop in a separate thread very simply inside your setUp method.

Something like this:

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