Scenario 5.2 released with new charm introspection API

Hi all! Scenario 5.2 just released with a new interface for introspecting the charm state during a scenario test execution.

Normally, a scenario test is a black-box, state-transition test.

state-in :arrow_right: :black_large_square::arrow_right:state-out

this leaves little room for getting a hold of the charm instance and, say, checking that my_charm.charm_lib._foo.bar(42) == "foo".

For a long time there has been an experimental, undocumented ‘callback’ API exposing pre-event and post-event hooks, callbacks that would be called with the charm instance before and after the event emission took place.

That API was however a bit obscure and unpythonic, so we refactored it to be a context manager:

from ops import CharmBase, StoredState

from charms.bar.lib_name.v1.charm_lib import CharmLib
from scenario import Context, State


class MyCharm(CharmBase):
    META = {"name": "mycharm"}
    _stored = StoredState()
    
    def __init__(self, framework):
        super().__init__(framework)
        self._stored.set_default(a="a")
        self.my_charm_lib = CharmLib()
        framework.observe(self.on.start, self._on_start)

    def _on_start(self, event):
        self._stored.a = "b"


def test_live_charm_introspection(mycharm):
    ctx = Context(mycharm, meta=mycharm.META)
    # If you want to do this with actions, you can use `Context.action_manager` instead.
    with ctx.manager("start", State()) as manager:
        # this is your charm instance, after ops has set it up
        charm: MyCharm = manager.charm
        
        # we can check attributes on nested Objects or the charm itself 
        assert charm.my_charm_lib.foo == "foo"
        # such as stored state
        assert charm._stored.a == "a"

        # this will tell ops.main to proceed with normal execution and emit the "start" event on the charm
        state_out = manager.run()
    
        # after that is done, we are handed back control, and we can again do some introspection
        assert charm.my_charm_lib.foo == "bar"
        # and check that the charm's internal state is as we expect
        assert charm._stored.a == "b"

    # state_out is, as in regular scenario tests, a State object you can assert on:
    assert state_out.unit_status == ...

Do you think that Context.manager() is cheeky? Totally agree, and you can blame @benhoyt for that idea. But we love it and we’re going to stick to it.

Give it a try and let us know!

1 Like