Unittest objects available on DBus: part 2
  |   Source

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.