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