1python-dbusmock
2===============
3
4Purpose
5-------
6With this program/Python library you can easily create mock objects on D-Bus.
7This is useful for writing tests for software which talks to D-Bus services
8such as upower, systemd, logind, gnome-session or others, and it is hard
9(or impossible without root privileges) to set the state of the real services
10to what you expect in your tests.
11
12Suppose you want to write tests for gnome-settings-daemon's power plugin, or
13another program that talks to upower. You want to verify that after the
14configured idle time the program suspends the machine. So your program calls
15``org.freedesktop.UPower.Suspend()`` on the system D-Bus.
16
17Now, your test suite should not really talk to the actual system D-Bus and the
18real upower; a ``make check`` that suspends your machine will not be considered
19very friendly by most people, and if you want to run this in continuous
20integration test servers or package build environments, chances are that your
21process does not have the privilege to suspend, or there is no system bus or
22upower to begin with. Likewise, there is no way for an user process to
23forcefully set the system/seat idle flag in logind, so your
24tests cannot set up the expected test environment on the real daemon.
25
26That's where mock objects come into play: They look like the real API (or at
27least the parts that you actually need), but they do not actually do anything
28(or only some action that you specify yourself). You can configure their
29state, behaviour and responses as you like in your test, without making any
30assumptions about the real system status.
31
32When using a local system/session bus, you can do unit or integration testing
33without needing root privileges or disturbing a running system. The Python API
34offers some convenience functions like ``start_session_bus()`` and
35``start_system_bus()`` for this, in a ``DBusTestCase`` class (subclass of the
36standard ``unittest.TestCase``).
37
38You can use this with any programming language, as you can run the mocker as a
39normal program. The actual setup of the mock (adding objects, methods,
40properties, and signals) all happen via D-Bus methods on the
41``org.freedesktop.DBus.Mock`` interface. You just don't have the convenience
42D-Bus launch API that way.
43
44
45Simple example in Python
46------------------------
47Picking up the above example about mocking upower's ``Suspend()`` method, this
48is how you would set up a mock upower in your test case:
49
50.. code-block:: python
51
52  import dbus
53  import dbusmock
54
55  class TestMyProgram(dbusmock.DBusTestCase):
56      @classmethod
57      def setUpClass(cls):
58          cls.start_system_bus()
59          cls.dbus_con = cls.get_dbus(system_bus=True)
60
61      def setUp(self):
62          self.p_mock = self.spawn_server('org.freedesktop.UPower',
63                                          '/org/freedesktop/UPower',
64                                          'org.freedesktop.UPower',
65                                          system_bus=True,
66                                          stdout=subprocess.PIPE)
67
68          # Get a proxy for the UPower object's Mock interface
69          self.dbus_upower_mock = dbus.Interface(self.dbus_con.get_object(
70              'org.freedesktop.UPower', '/org/freedesktop/UPower'),
71              dbusmock.MOCK_IFACE)
72
73          self.dbus_upower_mock.AddMethod('', 'Suspend', '', '', '')
74
75      def tearDown(self):
76          self.p_mock.stdout.close()
77          self.p_mock.terminate()
78          self.p_mock.wait()
79
80      def test_suspend_on_idle(self):
81          # run your program in a way that should trigger one suspend call
82
83          # now check the log that we got one Suspend() call
84          self.assertRegex(self.p_mock.stdout.readline(), b'^[0-9.]+ Suspend$')
85
86Let's walk through:
87
88 - We derive our tests from ``dbusmock.DBusTestCase`` instead of
89   ``unittest.TestCase`` directly, to make use of the convenience API to start
90   a local system bus.
91
92 - ``setUpClass()`` starts a local system bus, and makes a connection to it available
93   to all methods as ``dbus_con``. ``True`` means that we connect to the
94   system bus, not the session bus. We can use the same bus for all tests, so
95   doing this once in ``setUpClass()`` instead of ``setUp()`` is enough.
96
97 - ``setUp()`` spawns the mock D-Bus server process for an initial
98   ``/org/freedesktop/UPower`` object with an ``org.freedesktop.UPower`` D-Bus
99   interface on the system bus. We capture its stdout to be able to verify that
100   methods were called.
101
102   We then call ``org.freedesktop.DBus.Mock.AddMethod()`` to add a
103   ``Suspend()`` method to our new object to the default D-Bus interface. This
104   will not do anything (except log its call to stdout). It takes no input
105   arguments, returns nothing, and does not run any custom code.
106
107 - ``tearDown()`` stops our mock D-Bus server again. We do this so that each
108   test case has a fresh and clean upower instance, but of course you can also
109   set up everything in ``setUpClass()`` if tests do not interfere with each
110   other on setting up the mock.
111
112 - ``test_suspend_on_idle()`` is the actual test case. It needs to run your
113   program in a way that should trigger one suspend call. Your program will
114   try to call ``Suspend()``, but as that's now being served by our mock
115   instead of upower, there will not be any actual machine suspend. Our
116   mock process will log the method call together with a time stamp; you can
117   use the latter for doing timing related tests, but we just ignore it here.
118
119Simple example from shell
120-------------------------
121
122We use the actual session bus for this example. You can use
123``dbus-run-session`` to start a private one as well if you want, but that is
124not part of the actual mocking.
125
126So let's start a mock at the D-Bus name ``com.example.Foo`` with an initial
127"main" object on path /, with the main D-Bus interface
128``com.example.Foo.Manager``:
129
130::
131
132  python3 -m dbusmock com.example.Foo / com.example.Foo.Manager
133
134On another terminal, let's first see what it does:
135
136::
137
138  gdbus introspect --session -d com.example.Foo -o /
139
140You'll see that it supports the standard D-Bus ``Introspectable`` and
141``Properties`` interfaces, as well as the ``org.freedesktop.DBus.Mock``
142interface for controlling the mock, but no "real" functionality yet. So let's
143add a method:
144
145::
146
147  gdbus call --session -d com.example.Foo -o / -m org.freedesktop.DBus.Mock.AddMethod '' Ping '' '' ''
148
149Now you can see the new method in ``introspect``, and call it:
150
151::
152
153  gdbus call --session -d com.example.Foo -o / -m com.example.Foo.Manager.Ping
154
155The mock process in the other terminal will log the method call with a time
156stamp, and you'll see something like ``1348832614.970 Ping``.
157
158Now add another method with two int arguments and a return value and call it:
159
160::
161
162  gdbus call --session -d com.example.Foo -o / -m org.freedesktop.DBus.Mock.AddMethod \
163      '' Add 'ii' 'i' 'ret = args[0] + args[1]'
164  gdbus call --session -d com.example.Foo -o / -m com.example.Foo.Manager.Add 2 3
165
166This will print ``(5,)`` as expected (remember that the return value is always
167a tuple), and again the mock process will log the Add method call.
168
169You can do the same operations in e. g. d-feet or any other D-Bus language
170binding.
171
172Logging
173-------
174Usually you want to verify which methods have been called on the mock with
175which arguments. There are three ways to do that:
176
177 - By default, the mock process writes the call log to stdout.
178
179 - You can call the mock process with the ``-l``/``--logfile`` argument, or
180   specify a log file object in the ``spawn_server()`` method  if you are using
181   Python.
182
183 - You can use the ``GetCalls()``, ``GetMethodCalls()`` and ``ClearCalls()``
184   methods on the ``org.freedesktop.DBus.Mock`` D-Bus interface to get an array
185   of tuples describing the calls.
186
187
188Templates
189---------
190Some D-Bus services are commonly used in test suites, such as UPower or
191NetworkManager. python-dbusmock provides "templates" which set up the common
192structure of these services (their main objects, properties, and methods) so
193that you do not need to carry around this common code, and only need to set up
194the particular properties and specific D-Bus objects that you need. These
195templates can be parameterized for common customizations, and they can provide
196additional convenience methods on the ``org.freedesktop.DBus.Mock`` interface
197to provide more abstract functionality like "add a battery".
198
199For example, for starting a server with the "upower" template in Python you can
200run
201
202::
203
204  (self.p_mock, self.obj_upower) = self.spawn_server_template(
205      'upower', {'OnBattery': True}, stdout=subprocess.PIPE)
206
207or load a template into an already running server with the ``AddTemplate()``
208method; this is particularly useful if you are not using Python:
209
210::
211
212  python3 -m dbusmock --system org.freedesktop.UPower /org/freedesktop/UPower org.freedesktop.UPower
213
214  gdbus call --system -d org.freedesktop.UPower -o /org/freedesktop/UPower -m org.freedesktop.DBus.Mock.AddTemplate 'upower' '{"OnBattery": <true>}'
215
216This creates all expected properties such as ``DaemonVersion``, and changes the
217default for one of them (``OnBattery``) through the (optional) parameters dict.
218
219If you do not need to specify parameters, you can do this in a simpler way with
220
221::
222
223  python3 -m dbusmock --template upower
224
225The template does not create any devices by default. You can add some with
226the template's convenience methods like
227
228::
229
230  ac_path = self.dbusmock.AddAC('mock_AC', 'Mock AC')
231  bt_path = self.dbusmock.AddChargingBattery('mock_BAT', 'Mock Battery', 30.0, 1200)
232
233or calling ``AddObject()`` yourself with the desired properties, of course.
234
235If you want to contribute a template, look at dbusmock/templates/upower.py for
236a real-life implementation. You can copy dbusmock/templates/SKELETON to your
237new template file name and replace "CHANGEME" with the actual code/values.
238
239
240More Examples
241-------------
242Have a look at the test suite for two real-live use cases:
243
244 - ``tests/test_upower.py`` simulates upowerd, in a more complete way than in
245   above example and using the ``upower`` template. It verifies that
246   ``upower --dump`` is convinced that it's talking to upower.
247
248 - ``tests/test_api.py`` runs a mock on the session bus and exercises all
249   available functionality, such as adding additional objects, properties,
250   multiple methods, input arguments, return values, code in methods, raising
251   signals, and introspection.
252
253
254Documentation
255-------------
256The ``dbusmock`` module has extensive documentation built in, which you can
257read with e. g. ``pydoc3 dbusmock``.
258
259``pydoc3 dbusmock.DBusMockObject`` shows the D-Bus API of the mock object,
260i. e. methods like ``AddObject()``, ``AddMethod()`` etc. which are used to set
261up your mock object.
262
263``pydoc3 dbusmock.DBusTestCase`` shows the convenience Python API for writing
264test cases with local private session/system buses and launching the server.
265
266``pydoc3 dbusmock.templates`` shows all available templates.
267
268``pydoc3 dbusmock.templates.NAME`` shows the documentation and available
269parameters for the ``NAME`` template.
270
271``python3 -m dbusmock --help`` shows the arguments and options for running the
272mock server as a program.
273
274
275Development
276-----------
277python-dbusmock is hosted on github:
278
279  https://github.com/martinpitt/python-dbusmock
280
281Run the unit tests with
282
283    python3 -m unittest
284