Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(testing): update code samples in ops.testing from unittest to pytest style #1157

Merged
merged 7 commits into from
Mar 25, 2024
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Updated Pebble Notices `get_notices` parameter name to `users=all` (previously `select=all`).
* Added `Model.get_cloud_spec` which uses the `credential-get` hook tool to get details of the cloud where the model is deployed.
* Updated code examples in the docstring of `ops.testing` from unittest to pytest style.
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
* Refactored main.py, creating a new `_Manager` class.

# 2.11.0
Expand Down
137 changes: 67 additions & 70 deletions ops/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,47 +176,49 @@ class Harness(Generic[CharmType]):

The model created is from the viewpoint of the charm that is being tested.

Below is an example test using :meth:`begin_with_initial_hooks` that ensures
the charm responds correctly to config changes::
Always call ``harness.cleanup()`` after creating a :class:`Harness`::
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

class TestCharm(unittest.TestCase):
def test_foo(self):
harness = Harness(MyCharm)
self.addCleanup(harness.cleanup) # always clean up after ourselves
@pytest.fixture()
def harness():
harness = Harness(MyCharm)
yield harness
harness.cleanup()

# Instantiate the charm and trigger events that Juju would on startup
harness.begin_with_initial_hooks()
Below is an example test using :meth:`begin_with_initial_hooks` that ensures
the charm responds correctly to config changes (the parameter ``harness`` in the
test function is a pytest fixture that does setup/teardown, see :class:`Harness`)::

# Update charm config and trigger config-changed
harness.update_config({'log_level': 'warn'})
def test_foo(harness):
# Instantiate the charm and trigger events that Juju would on startup
harness.begin_with_initial_hooks()

# Check that charm properly handled config-changed, for example,
# the charm added the correct Pebble layer
plan = harness.get_container_pebble_plan('prometheus')
self.assertIn('--log.level=warn', plan.services['prometheus'].command)
# Update charm config and trigger config-changed
harness.update_config({'log_level': 'warn'})

# Check that charm properly handled config-changed, for example,
# the charm added the correct Pebble layer
plan = harness.get_container_pebble_plan('prometheus')
assert '--log.level=warn' in plan.services['prometheus'].command

To set up the model without triggering events (or calling charm code), perform the
harness actions before calling :meth:`begin`. Below is an example that adds a
relation before calling ``begin``, and then updates config to trigger the
``config-changed`` event in the charm::

class TestCharm(unittest.TestCase):
def test_bar(self):
harness = Harness(MyCharm)
self.addCleanup(harness.cleanup) # always clean up after ourselves
``config-changed`` event in the charm (the parameter ``harness`` in the test function
is a pytest fixture that does setup/teardown, see :class:`Harness`)::

# Set up model before "begin" (no events triggered)
harness.set_leader(True)
harness.add_relation('db', 'postgresql', unit_data={'key': 'val'})
def test_bar(harness):
# Set up model before "begin" (no events triggered)
harness.set_leader(True)
harness.add_relation('db', 'postgresql', unit_data={'key': 'val'})

# Now instantiate the charm to start triggering events as the model changes
harness.begin()
harness.update_config({'some': 'config'})
# Now instantiate the charm to start triggering events as the model changes
harness.begin()
harness.update_config({'some': 'config'})

# Check that charm has properly handled config-changed, for example,
# has written the app's config file
root = harness.get_filesystem_root('container')
assert (root / 'etc' / 'app.conf').exists()
# Check that charm has properly handled config-changed, for example,
# has written the app's config file
root = harness.get_filesystem_root('container')
assert (root / 'etc' / 'app.conf').exists()

Args:
charm_cls: The Charm class to test.
Expand Down Expand Up @@ -962,8 +964,8 @@ def add_relation_unit(self, relation_id: int, remote_unit_name: str) -> None:

Example::

rel_id = harness.add_relation('db', 'postgresql')
harness.add_relation_unit(rel_id, 'postgresql/0')
rel_id = harness.add_relation('db', 'postgresql')
harness.add_relation_unit(rel_id, 'postgresql/0')
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

Args:
relation_id: The integer relation identifier (as returned by :meth:`add_relation`).
Expand Down Expand Up @@ -1004,10 +1006,10 @@ def remove_relation_unit(self, relation_id: int, remote_unit_name: str) -> None:

Example::

rel_id = harness.add_relation('db', 'postgresql')
harness.add_relation_unit(rel_id, 'postgresql/0')
...
harness.remove_relation_unit(rel_id, 'postgresql/0')
rel_id = harness.add_relation('db', 'postgresql')
harness.add_relation_unit(rel_id, 'postgresql/0')
...
harness.remove_relation_unit(rel_id, 'postgresql/0')
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

This will trigger a `relation_departed` event. This would
normally be followed by a `relation_changed` event triggered
Expand Down Expand Up @@ -1698,7 +1700,8 @@ def get_filesystem_root(self, container: Union[str, Container]) -> pathlib.Path:
ownership. To circumvent this limitation, the testing harness maps all user and group
options related to file operations to match the current user and group.

Example usage::
Example usage (the parameter ``harness`` in the test function is a pytest fixture
that does setup/teardown, see :class:`Harness`)::

# charm.py
class ExampleCharm(ops.CharmBase):
Expand All @@ -1711,15 +1714,12 @@ def _on_pebble_ready(self, event: ops.PebbleReadyEvent):
self.hostname = event.workload.pull("/etc/hostname").read()

# test_charm.py
class TestCharm(unittest.TestCase):
def test_hostname(self):
harness = Harness(ExampleCharm)
self.addCleanup(harness.cleanup)
root = harness.get_filesystem_root("mycontainer")
(root / "etc").mkdir()
(root / "etc" / "hostname").write_text("hostname.example.com")
harness.begin_with_initial_hooks()
assert harness.charm.hostname == "hostname.example.com"
def test_hostname(harness):
root = harness.get_filesystem_root("mycontainer")
(root / "etc").mkdir()
(root / "etc" / "hostname").write_text("hostname.example.com")
harness.begin_with_initial_hooks()
assert harness.charm.hostname == "hostname.example.com"

Args:
container: The name of the container or the container instance.
Expand Down Expand Up @@ -1916,8 +1916,10 @@ def set_cloud_spec(self, spec: 'model.CloudSpec'):

Call this method before the charm calls :meth:`ops.Model.get_cloud_spec`.

Example usage::
Example usage (the parameter ``harness`` in the test function is
a pytest fixture that does setup/teardown, see :class:`Harness`)::

# charm.py
class MyVMCharm(ops.CharmBase):
def __init__(self, framework: ops.Framework):
super().__init__(framework)
Expand All @@ -1926,30 +1928,25 @@ def __init__(self, framework: ops.Framework):
def _on_start(self, event: ops.StartEvent):
self.cloud_spec = self.model.get_cloud_spec()

class TestCharm(unittest.TestCase):
def setUp(self):
self.harness = ops.testing.Harness(MyVMCharm)
self.addCleanup(self.harness.cleanup)

def test_start(self):
cloud_spec_dict = {
'name': 'localhost',
'type': 'lxd',
'endpoint': 'https://127.0.0.1:8443',
'credential': {
'authtype': 'certificate',
'attrs': {
'client-cert': 'foo',
'client-key': 'bar',
'server-cert': 'baz'
},
# test_charm.py
def test_start(harness):
cloud_spec = ops.model.CloudSpec.from_dict({
'name': 'localhost',
'type': 'lxd',
'endpoint': 'https://127.0.0.1:8443',
'credential': {
'auth-type': 'certificate',
'attrs': {
'client-cert': 'foo',
'client-key': 'bar',
'server-cert': 'baz'
},
}
self.harness.set_cloud_spec(ops.CloudSpec.from_dict(cloud_spec_dict))
self.harness.begin()
self.harness.charm.on.start.emit()
expected = ops.CloudSpec.from_dict(cloud_spec_dict)
self.assertEqual(harness.charm.cloud_spec, expected)
},
})
harness.set_cloud_spec(cloud_spec)
harness.begin()
harness.charm.on.start.emit()
assert harness.charm.cloud_spec == cloud_spec

"""
self._backend._cloud_spec = spec
Expand Down
Loading