A relation data wrapper for unit and integration testing

As I worked on jhack show-relation it became apparent how having a clearly structured way of addressing relation data is fundamental to being able to understand the ‘relation’ data structure itself.

Show-relation reasons in terms of:

  • provider:
    • app data: Dict[str, str]
    • unit data: Dict[int, Dict[str, str]
    • metadata:
      • scale: int
      • units: Tuple[int] # the IDs of the units of this application
      • leader_id: int # which one of the units is leader
      • interface: str # name of the relation interface used by this provider
  • requirer: same tree as provider.

Then I thought, wouldn’t it be nice if the testing code we wrote, regardless of it’s unit tests using the Harness, or integration tests using python-libjuju (or any other framework, for that matters), also used the same structure?

I would like my tests to be like:

def test_relation_data():
    data = get_data(provider_endpoint='my_app:db', requirer_endpoint='remote:db')
    assert data.provider.app_data['foo'] == 'bar'
    for unit in data.requirer.meta.units:
         assert data.requirer.units_data[unit]['baz'] == 'qux'

Fortunately, the engine for producing this structure was already written! I extracted the source code from jhack show-relation and packaged it in a standalone charm lib under the harness-extensions collection, as it’s about facilitating testing.

How to use

charmcraft fetch-lib charms.harness_extensions.v0.relation_data_wrapper

# for integration tests, pytest-operator, or zaza context:
from charms.harness_extensions.v0.relation_data_wrapper import get_relation_data_from_juju 

# for unit tests:
from charms.harness_extensions.v0.relation_data_wrapper import get_relation_data_from_harness

def test_unit(h:Harness):
    r_id = h.add_relation('ingress', 'remote')
    h.add_relation_unit(r_id, 'remote/0')
    h.add_relation_unit(r_id, 'remote/1')

    h.update_relation_data(r_id, 'local', {'lapp': 'data'})
    h.update_relation_data(r_id, 'local/0', {'lunit0': 'data0'})
    h.update_relation_data(r_id, 'remote', {'rapp': 'data'})
    h.update_relation_data(r_id, 'remote/0', {'unit0': 'data0'})
    h.update_relation_data(r_id, 'remote/1', {'unit1': 'data1'})

    rdata = get_relation_data_from_harness(
        h,
        provider_endpoint='local:ingress',
        requirer_endpoint='remote:ingress'
    )


def test_integration(ops_test):
    # deploy and relate traefik ('trfk') and prometheus ('prom') ...
    rdata = get_relation_data_from_juju(requirer_endpoint='prom:ingress',
                                        provider_endpoint='trfk:ingress-per-unit')
    assert rdata.provider.app_name == 'trfk'
    assert set(rdata.provider.units_data) == {0}


At the moment this is a proof of concept, but if it takes off, why not, we could include this in the Harness, python-libjuju, zaza, or whatever testing framework you use!

1 Like

I think this is a really good idea as I have not found any other easy way of getting the relation data. However, when using it on a relation when one of the charms is a subordinate charm I get a KeyError: 'units' in expression scale = len(status['applications'][app_name]['units']). It makes sense as the subordinate does not have a units key. Could not find where this code lived so I reported it here.

Also since this is almost a year old I would like to ask: have you @ppasotti found any other easy way of getting the relation data?

Hey thanks for the interest! You can report bugs and issues at https://github.com/PietroPasotti/harness-extensions

And no, unfortunately no other solution has emerged in the meantime.

1 Like