Skip to main content

Unittest object/interface available on DBus

There is better way to do this.

This is in continuation of one of my old post about writing unittests and mocking. In this post we will cover three points:

Write unittest for a DBus Object

Ideally an object exposed over DBus, is a regular object which can be created, accessed and tested like any other normal class in python. We had created our object/interface based on examples in dbus-python and from this blog post series. There DBus is very inherently coupled with the class making it impossible to create independent object. Like for example, I am using special decorators to expose methods over DBus. Because of this limitation, while writing unittest for this class, we ran into unique situation where, DBus service has to be running while we test.

Furthermore, DBus objects needs an ever running eventloop over which they are made available. dbus-python uses GLib main-loop, so we need to figure out a way by which, when we run tests, we are able to start this eventloop, make our object available over it, and then run unittest against it. While looking for answer StackOverflow came to rescue and I came across this thread and one of participant whose answer/comment contributed to the final solution says:

This is easy, not hard. And you MUST do it. Don't ever skimp on unit test 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.

The solution is to starts an independent process where DBus object is initialized and connected to eventloop running in that process, before running the unittest. Here is sample code, similar to solution suggested in StackOverflow:

class TestServices(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
	# we start eventloop and make our class available on it
	cls.p = subprocess.Popen(['python3', '-m', test_services', 'server'])
	# This was needed to wait for service to become available
	time.sleep(2)
	assert cls.p.stdout == None
	assert cls.p.stderr == None

    @classmethod
    def tearDownClass(cls):
	# This is needed to clean up event loop we started in
	# setUpClass
	os.kill(cls.p.pid, 15)

    def setUp(self):
	bus = dbus.SessionBus()
	handler = bus.get_object("example.org",
				 "/example/org/DemoService")

    def test_add_component_random_strings(self):
	success, message = self.handler.demo_method('random string')
	self.assertFalse(success)

if __name__ == '__main__':
    arg = ""
    if len(sys.argv) > 1:
	arg = sys.argv[1]
	if arg == "server":
	    loop = GLib.MainLoop()
	    DBusGMainLoop(set_as_default=True)
	    bus_name = dbus.service.BusName("example.org",
					    bus=dbus.SessionBus(),
					    do_not_queue=True)
	    DemoService(bus_name)
	    try:
		loop.run()
	    except KeyboardInterrupt:
		pass
	    loop.quit()
	else:
	    unittest.main()

Getting dbus running on Travis-CI

We have Travis-CI and docker setup for running tests. With docker as I tried to run tests, it failed with:

======================================================================
ERROR: test_add_component_random_strings (test_services.TestServices)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/opt/app/test_services.py", line 135, in test_add_component_random_strings
    bus = dbus.SessionBus()
  File "/usr/lib/python3/dist-packages/dbus/_dbus.py", line 211, in __new__
    mainloop=mainloop)
  File "/usr/lib/python3/dist-packages/dbus/_dbus.py", line 100, in __new__
    bus = BusConnection.__new__(subclass, bus_type, mainloop=mainloop)
  File "/usr/lib/python3/dist-packages/dbus/bus.py", line 122, in __new__
    bus = cls._new_for_bus(address_or_type, mainloop=mainloop)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NotSupported: Unable to autolaunch a dbus-daemon without a $DISPLAY for X11

We are exposing our object over SessionBus, so we have to start DBus session-bus on docker image to run our unittest. I looked for other python based repository on github using DBus and passing travis-ci tests. I came across this really well written project: pass_secret_service and in its Makefile we found our solution:

bash -c 'dbus-run-session -- python3 -m unittest -v'

dbus-run-session starts SessionBus on docker image and that's exactly what we wanted. Apart from this solution, this project had even more better and cleaner way to unittest, it has a decorator which takes care of exporting the object over DBus. So far, I wasn't able to get this solution working for me. The project uses pydbus instead of dbus-python, ideally I should be able to shift to it, but will have to try that.

Mock object which are accessible to DBus object

Generally when we need to mock a behaviour, we can use patch decorator from mock library and set relevant behaviour(attribute of return_value or side-effect). But given the peculiarity of above setup, tests are running in a different process. So mocking behaviour around the unittest won't work, because DBus object is in different process and it won't have access to these mocked objects. To get around this we will need to mock things just before we start the MainLoop and create DemoService object:

with mock.patch('hue.Bridge') as MockBridge:
    with mock.patch('configparser') as mock_config:
	with mock.patch('requests') as mock_requests:
	    MockBridge.return_value.get_light.return_value = lights_dict
	    loop = GLib.MainLoop()
	    DBusGMainLoop(set_as_default=True)
	    bus_name = dbus.service.BusName("example.org",
					    bus=dbus.SessionBus(),
					    do_not_queue=True)
	    DemoService(bus_name)
	    try:
		loop.run()
	    except KeyboardInterrupt:
		pass
	    loop.quit()