diff --git a/README.md b/README.md index 5238b6ee..d8149874 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: @@ -125,6 +125,80 @@ 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 +import subprocess + +import dbus +import pytest + +import dbusmock + + +@pytest.fixture +def upower_mock(dbusmock_testcase_system): + p_mock = dbusmock_testcase_system.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(dbusmock_testcase_system.get_dbus(True).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 upower_mock.stdout.readline() == b'^[0-9.]+ Suspend$' +``` + +Let's walk through: + +- We import the `system_mock` fixture from dbusmock which provides us + with a system bus started for our test case wherever the + `dbusmock_testcase_system` 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 @@ -301,9 +375,7 @@ the mock server as a program. python-dbusmock is hosted on https://github.com/martinpitt/python-dbusmock -Run the unit tests with - - python3 -m unittest +Run the unit tests with `python3 -m unittest` or `pytest`. In CI, the unit tests run in containers. You can run them locally with e.g. diff --git a/dbusmock/pytest_fixtures.py b/dbusmock/pytest_fixtures.py new file mode 100644 index 00000000..459a35bb --- /dev/null +++ b/dbusmock/pytest_fixtures.py @@ -0,0 +1,43 @@ +'''pytest fixtures for DBusMock''' + +# 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. + +__author__ = 'Martin Pitt' +__copyright__ = '(c) 2023 Martin Pitt ' + +from typing import Iterator + +import pytest + +import dbusmock.testcase + + +@pytest.fixture(name='dbusmock_testcase', scope='session') +def fixture_dbusmock_testcase() -> Iterator[dbusmock.testcase.DBusTestCase]: + '''Export the whole DBusTestCase as a fixture.''' + + testcase = dbusmock.testcase.DBusTestCase() + testcase.setUp() + yield testcase + testcase.tearDown() + testcase.tearDownClass() + + +@pytest.fixture(scope='session') +def dbusmock_testcase_system(dbusmock_testcase) -> dbusmock.testcase.DBusTestCase: + '''Export the whole DBusTestCase as a fixture, with system bus started''' + + dbusmock_testcase.start_system_bus() + return dbusmock_testcase + + +@pytest.fixture(scope='session') +def dbusmock_testcase_session(dbusmock_testcase) -> dbusmock.testcase.DBusTestCase: + '''Export the whole DBusTestCase as a fixture, with session bus started''' + + dbusmock_testcase.start_session_bus() + return dbusmock_testcase diff --git a/packaging/python-dbusmock.spec b/packaging/python-dbusmock.spec index 31d79ece..22532fbd 100644 --- a/packaging/python-dbusmock.spec +++ b/packaging/python-dbusmock.spec @@ -15,6 +15,7 @@ BuildRequires: python3-dbus BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-gobject +BuildRequires: python3-pytest BuildRequires: dbus-x11 BuildRequires: upower diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..ab7ecbaf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = "dbusmock.pytest_fixtures" diff --git a/tests/run-debian b/tests/run-debian index 04d9ba9d..093b7ac2 100644 --- a/tests/run-debian +++ b/tests/run-debian @@ -13,7 +13,7 @@ eatmydata apt-get -y --purge dist-upgrade # install build dependencies eatmydata apt-get install --no-install-recommends -y git \ python3-all python3-setuptools python3-setuptools-scm python3-build python3-venv \ - python3-dbus python3-gi gir1.2-glib-2.0 \ + python3-dbus python3-pytest python3-gi gir1.2-glib-2.0 \ dbus libnotify-bin upower network-manager bluez ofono ofono-scripts # systemd's tools otherwise fail on "not been booted with systemd" @@ -27,6 +27,7 @@ export TEST_CODE="$TEST_CODE" cp -r $(pwd) /tmp/source cd /tmp/source python3 -m unittest -v +python3 -m pytest -vv -k 'test_pytest or TestAPI' # massively parallel test to check for races for i in \$(seq 100); do ( PYTHONPATH=. python3 tests/test_api.py TestTemplates || touch /tmp/fail ) & diff --git a/tests/run-fedora b/tests/run-fedora index 7f0ed48b..4098115f 100644 --- a/tests/run-fedora +++ b/tests/run-fedora @@ -2,7 +2,7 @@ set -eux # install build dependencies dnf -y install python3-setuptools python3 python3-gobject-base \ - python3-dbus dbus-x11 util-linux \ + python3-dbus python3-pytest dbus-x11 util-linux \ upower NetworkManager bluez libnotify polkit if ! grep -q :el /etc/os-release; then @@ -19,12 +19,17 @@ mkdir -p /run/systemd/system # run build and test as user useradd build -su -s /bin/sh - build << EOF +su -s /bin/sh - build << EOF || { [ -z "$DEBUG" ] || sleep infinity; exit 1; } set -ex cd /source export TEST_CODE="$TEST_CODE" -python3 -m unittest -v || { - [ -z "$DEBUG" ] || sleep infinity - exit 1 -} + +python3 -m unittest -v + +# run all tests with pytest only on TEST_CODE, as that is expensive +if [ -n "$TEST_CODE" ]; then + python3 -m pytest -v +else + python3 -m pytest -vv -k 'test_pytest or TestAPI' +fi EOF diff --git a/tests/test_api_pytest.py b/tests/test_api_pytest.py new file mode 100644 index 00000000..25f00a06 --- /dev/null +++ b/tests/test_api_pytest.py @@ -0,0 +1,35 @@ +import subprocess +import tempfile + +import pytest + +import dbusmock + + +def test_dbusmock_testcase_spawn_server(dbusmock_testcase_session): + test_iface = 'org.freedesktop.Test.Main' + + p_mock = dbusmock_testcase_session.spawn_server( + 'org.freedesktop.Test', '/', test_iface, stdout=tempfile.TemporaryFile()) + + obj_test = dbusmock_testcase_session.get_dbus().get_object('org.freedesktop.Test', '/') + + obj_test.AddMethod('', 'Upper', 's', 's', 'ret = args[0].upper()', interface_name=dbusmock.MOCK_IFACE) + assert obj_test.Upper('hello', interface=test_iface) == 'HELLO' + + p_mock.terminate() + p_mock.wait() + + +@pytest.fixture +def upower_mock(dbusmock_testcase_system): + p_mock, obj = dbusmock_testcase_system.spawn_server_template('upower', stdout=subprocess.DEVNULL) + yield obj + p_mock.terminate() + p_mock.wait() + + +def test_dbusmock_testcase_spawn_system_template(upower_mock): + out = subprocess.check_output(['upower', '--dump'], universal_newlines=True) + assert 'version:' in out + assert '0.99' in out