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.