How to write scenario tests for a charm

See also: Scenario (scenario)

First of all, install scenario

pip install ops-scenario

Then, open a new test_foo.py file where you will put the test code.

# import the necessary objects from scenario and ops
from scenario import State, Context
import ops

See more: State (Scenario).

Then declare a new charm type:

class MyCharm(ops.CharmBase):
    pass        

And finally we can write a test function. The test code should use a Context object to encapsulate the charm type being tested (MyCharm) and any necessary metadata, then declare the initial State the charm will be presented when run, and run the context with an event and that initial state as parameters. In code:

def test_charm_runs():
    # arrange: 
    #  create a Context to specify what code we will be running
    ctx = Context(MyCharm, meta={'name': 'my-charm'})
    #  and create a State to specify what simulated data the charm being run will access
    state_in = State()
    # act:
    #  ask the context to run an event, e.g. 'start', with the state we have previously created
    state_out = ctx.run('start', state_in)
    # assert:
    #  verify that the output state looks like you expect it to
    assert state_out.status.unit.name == 'unknown' 

See more:

If you like using unittest, you should rewrite this as a method of some TestCase subclass.

Mocking beyond the State

If you wish to use Scenario to test an existing charm type, you will probably need to mock out certain calls that are not covered by the State data structure. In that case, you will have to manually mock, patch or otherwise simulate those calls on top of what Scenario does for you.

For example, suppose that the charm we’re testing uses the KubernetesServicePatch. To update the test above to mock that object, modify the test file to contain:

import pytest
from unittest import patch

@pytest.fixture
def my_charm():
    with patch("charm.KubernetesServicePatch"):
        yield MyCharm

Then you should rewrite the test to pass the patched charm type to the Context, instead of the unpatched one. In code:

def test_charm_runs(my_charm):
    # arrange: 
    #  create a Context to specify what code we will be running
    ctx = Context(my_charm, meta={'name': 'my-charm'})
    # ...

If you use pytest, you should put the my_charm fixture in a toplevel conftest.py, as it will likely be shared between all your scenario tests.