Skip to content

Commit

Permalink
Add a pytest module to provide a simple fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
whot committed Aug 11, 2023
1 parent 83b4850 commit 80233a2
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 3 deletions.
73 changes: 70 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions dbusmock/pytest.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest_plugins = "dbusmock.pytest"
52 changes: 52 additions & 0 deletions tests/test_api_pytest.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 80233a2

Please sign in to comment.