Unittest objects available on DBus: part 2
This is a follow up of my previous post. Basically I am learning
things about mock library and DBus as we are removing technical
debt, addressing our pain points and making product stable.
Partial mock DBus to unittest Object exported over DBus
last time I wrote about the pain to test object available on DBus. I
got it reviewed with my teammates. As I paired with one, he agreed
that with such a setup, these tests are not unittest but more like
system or integration test.
First thing, let me share how I am exporting Object on dbus:
#!/usr/bin/env python3
import dbus
import dbus.service
import json
INTERFACE_NAME = "org.example.ControlInterface"
class ControlObject(dbus.service.Object):
def __init__(self, bus_name, conf):
super().__init__(bus_name, "/org/example/ControlObject")
self._conf = conf
print("Started service")
@dbus.service.method(INTERFACE_NAME,
in_signature='s', out_signature='b')
def Update(self, conf):
try:
self._conf = json.loads(conf)
except json.JSONDecodeError:
print('Could not parse json')
raise
@dbus.service.method(INTERFACE_NAME,
in_signature='s',
out_signature='s')
def Get(self, key=''):
if key == '':
# we can have empty strings as key to dict in python
raise KeyError
try:
components = _conf[key]
except (KeyError, TypeError):
raise
else:
try:
component = next(c for c in components if c['id'] == component_id)
except StopIteration:
raise
else:
return json.dump(component)
This Object takes a bus_name argument for initializing:
try:
bus_name = dbus.service.BusName("org.example.ControlInterface",
bus=dbus.SystemBus(),
do_not_queue=True)
except dbus.exceptions.NameExistsException:
logger.info("BusName is already used by some different service")
else:
ControlObject(bus_name, {})
This way of setting up things coupled my Object to DBus setup
tightly. I have to pass this bus_name as argument. As I was getting
it reviewed with another of my colleague, he mentioned that I should
be able to patch dbus and possibly get around with way I was setting up system with DBus, export object to it and then run test.
I had used partial mocking using with construct of mock, I put
together following test using it:
from unittest import mock
from unittest import TestCase
import json
from service import ControlObject
class TestService(TestCase):
def setUp(self):
with mock.patch('service.dbus.SystemBus') as MockBus:
self.obj = ControlObject(MockBus, {})
def tearDown(self):
del self.obj
def test_object_blank_state(self):
self.assertFalse(self.obj._conf)
def test_object_update_random_string(self):
exception_string = 'Expecting value'
with self.assertRaises(json.JSONDecodeError) as context:
self.assertFalse(self.obj.Update(''))
self.assertIn(exception_string, context.exception.msg)
self.assertFalse(self.obj._conf)
with self.assertRaises(json.JSONDecodeError) as context:
self.assertFalse(self.obj.Update('random string'))
self.assertIn(exception_string, context.exception.msg)
self.assertFalse(self.obj._conf)
def test_object_update(self):
conf = {'id': 'id',
'name': 'name',
'station': 'station'}
self.obj.Update(json.dumps(conf))
self.assertTrue(self.obj._conf)
for key in conf:
self.assertTrue(key in self.obj._conf)
self.assertEqual(conf[key], self.obj._conf[key])
This test worked directly and there was no need to, setup
dbus-session on docker image, run a process where I export the
object and call methods over DBus. Instead now, I can directly access
all attributes and methods of the Object and write better tests.
$ python -m unittest -v test_service.py
test_object_blank_state (test_service.TestService) ... ok
test_object_update (test_service.TestService) ... ok
test_object_update_random_string (test_service.TestService) ... Could not parse json
Could not parse json
ok
----------------------------------------------------------------------
Ran 3 tests in 0.002s
OK
Another way(Better?) to export Object over DBus
While writing this post and exploring examples from dbus-python I found another way to write class which can be exported over DBus:
class ControlObject(dbus.service.Object):
def __init__(self, conf, *args, **kwargs):
super().__init__(*args, **kwargs)
self._conf = conf
print("Started service")
Now we don't even need to claim BusName and pass it as argument to
this class. Instead we can make this Object available on DBus by:
system_bus = dbus.SystemBus()
bus_name = dbus.service.BusName('org.example.ControlObject',
system_bus)
ComponentsService({}, system_bus, '/org/example/ControlObject')
And when we don't want to use DBus and just create instance of this
Object we can directly do that also, by calling
ComponentsService({}) directly. With this way of initializing, we
don't need to partial mock DBus and write unittest directly.