diff --git a/README.md b/README.md index 45f38384..2343dc4d 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ When using a local system/session bus, you can do unit or integration testing without needing root privileges or disturbing a running system. The Python API offers some convenience functions like `start_session_bus()` and `start_system_bus()` for this, in a -`DBusTestCase` class (subclass of the standard `unittest.TestCase`). +`DBusTestCase` class (subclass of the standard `unittest.TestCase`) or +alternatively as a `@pytest.fixture`. You can use this with any programming language, as you can run the mocker as a normal program. The actual setup of the mock (adding objects, methods, properties, and signals) all happen via D-Bus methods on the `org.freedesktop.DBus.Mock` interface. You just don't have the convenience D-Bus launch API that way. - -## Simple example in Python +## Simple example using Python's unittest Picking up the above example about mocking upower's `Suspend()` method, this is how you would set up a mock upower in your test case: @@ -121,6 +121,73 @@ Let's walk through: together with a time stamp; you can use the latter for doing timing related tests, but we just ignore it here. +## Simple example using pytest + +The same functionality as above but instead using the pytest fixture provided +by this package. + +```python +from dbusmock.pytest import session_mock +import dbus + +@pytest.fixture +def upower_mock(system_mock): + p_mock = system_mock.spawn_server('org.freedesktop.UPower', + '/org/freedesktop/UPower', + 'org.freedesktop.UPower', + system_bus=True, + stdout=subprocess.PIPE) + + # Get a proxy for the UPower object's Mock interface + dbus_upower_mock = dbus.Interface(system_mock.dbus_con.get_object( + 'org.freedesktop.UPower', + '/org/freedesktop/UPower' + ), dbusmock.MOCK_IFACE) + dbus_upower_mock.AddMethod('', 'Suspend', '', '', '') + + yield p_mock + + p_mock.stdout.close() + p_mock.terminate() + p_mock.wait() + + +def test_suspend_on_idle(upower_mock): + # run your program in a way that should trigger one suspend call + + # now check the log that we got one Suspend() call + assert self.p_mock.stdout.readline() == b'^[0-9.]+ Suspend$' +``` + +Let's walk through: + +- We import the `session_mock` fixture from dbusmock which provides us + with a session bus started for our test case wherever the `session_mock` + argument is used by a test case and/or a pytest fixture. + +- The `upower_mock` fixture spawns the mock D-Bus server process for an initial + `/org/freedesktop/UPower` object with an `org.freedesktop.UPower` + D-Bus interface on the system bus. We capture its stdout to be + able to verify that methods were called. + + We then call `org.freedesktop.DBus.Mock.AddMethod()` to add a + `Suspend()` method to our new object to the default D-Bus + interface. This will not do anything (except log its call to + stdout). It takes no input arguments, returns nothing, and does + not run any custom code. + + This mock server process is yielded to the test function that uses + the `upower_mock` fixture - once the test is complete the process is + terminated again. + +- `test_suspend_on_idle()` is the actual test case. It needs to run + your program in a way that should trigger one suspend call. Your + program will try to call `Suspend()`, but as that's now being + served by our mock instead of upower, there will not be any actual + machine suspend. Our mock process will log the method call + together with a time stamp; you can use the latter for doing + timing related tests, but we just ignore it here. + ## Simple example from shell We use the actual session bus for this example. You can use diff --git a/dbusmock/pytest.py b/dbusmock/pytest.py new file mode 100644 index 00000000..5883c4c1 --- /dev/null +++ b/dbusmock/pytest.py @@ -0,0 +1,58 @@ +'''pytest convenience methods for DBusMocks''' + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) any +# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text +# of the license. + +import pytest +from typing import Iterator, Optional + +import pytest +import dbus +import dbusmock + + +class BusMock(dbusmock.DBusTestCase): + def __init__(self): + super().__init__() + self._dbus_con: Optional[dbus.Bus] = None + + @property + def dbus_con(self) -> "dbus.Bus": + ''' + Returns the dbus.bus.BusConnection() for this object. This returns either + the session bus connection or the system bus connection, depending on the + object. + ''' + assert self._dbus_con is not None + return self._dbus_con + + +@pytest.fixture(scope='session') +def session_mock() -> Iterator[BusMock]: + ''' + Fixture to yield a DBusTestCase with a started session bus. + ''' + bus = BusMock() + bus.setUp() + bus.start_session_bus() + bus._dbus_con = bus.get_dbus(system_bus=False) + yield bus + bus.tearDown() + bus.tearDownClass() + + +@pytest.fixture(scope='session') +def system_mock() -> Iterator[BusMock]: + ''' + Fixture to yield a DBusTestCase with a started session bus. + ''' + bus = BusMock() + bus.setUp() + bus.start_system_bus() + bus._dbus_con = bus.get_dbus(system_bus=True) + yield bus + bus.tearDown() + bus.tearDownClass() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..4309d898 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = "dbusmock.pytest" diff --git a/tests/test_api_pytest.py b/tests/test_api_pytest.py new file mode 100644 index 00000000..691f571c --- /dev/null +++ b/tests/test_api_pytest.py @@ -0,0 +1,52 @@ +#!/usr/bin/python3 + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) any +# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text +# of the license. + +from pathlib import Path +import dbus +import dbusmock +import re +import tempfile + +try: + from dbusmock.pytest import session_mock + import pytest + + @pytest.fixture + def mock(session_mock): + # pylint: disable=consider-using-with + session_mock.mock_log = tempfile.NamedTemporaryFile() + session_mock.p_mock = session_mock.spawn_server('org.freedesktop.Test', + '/', + 'org.freedesktop.Test.Main', + stdout=session_mock.mock_log) + + session_mock.obj_test = session_mock.dbus_con.get_object('org.freedesktop.Test', '/') + session_mock.dbus_test = dbus.Interface(session_mock.obj_test, 'org.freedesktop.Test.Main') + session_mock.dbus_mock = dbus.Interface(session_mock.obj_test, dbusmock.MOCK_IFACE) + session_mock.dbus_props = dbus.Interface(session_mock.obj_test, dbus.PROPERTIES_IFACE) + yield session_mock + + if session_mock.p_mock.stdout: + session_mock.p_mock.stdout.close() + session_mock.p_mock.terminate() + session_mock.p_mock.wait() + + + class TestPytestAPI: + def test_noarg_noret(self, mock): + '''no arguments, no return value''' + + mock.dbus_mock.AddMethod('', 'Do', '', '', '') + assert mock.dbus_test.Do() == None + + # check that it's logged correctly + log = Path(mock.mock_log.name,).read_bytes() + assert re.match(rb'^[0-9.]+ Do$', log) + +except ImportError: + pass