Scenario provides a Relation
dataclass that wraps all mock relation data one might possibly need when testing charm relation code.
Relation data is nested in State under State.relations
.
Therefore, in order to set up a scenario test with relations, one does:
from scenario import State, Relation
state = State(relations=[
Relation("endpoint_1"),
Relation("endpoint_2"),
...
])
And in order to verify the state of a relation after an event has been fired, one does:
from scenario import Context
state_out = Context(...).run(...)
for relation in state_out.relations:
if relation.endpoint == "endpoint_1":
assert relation.local_unit_data == {...}
# or you can grab relations by endpoint name:
relations_1 = state_out.get_relations("endpoint_1")
...
The only mandatory argument to Relation
(and other relation types, see below) is endpoint
. The interface
will be derived from the charm’s metadata. When fully defaulted, a relation is ‘empty’. There are no remote units, the remote application is called 'remote'
and only has a single unit remote/0
, and the databags are still empty.
That is typically the state of a relation when the first unit (local or remote) joins it.
Non-regular relations
When you use Relation
, you are specifying a regular (conventional) relation. But that is not the only type of relation. There are also peer relations and subordinate relations. While in the background the data model is the same, the data access rules and the consistency constraints on them are very different. For example, it does not make sense for a peer relation to have a different ‘remote app’ than its ‘local app’, because it’s the same application.
PeerRelation
To declare a peer relation, you should use scenario.state.PeerRelation
. The core difference with regular relations is
that peer relations do not have a “remote app” (it’s this app, in fact). So unlike Relation
, a PeerRelation
does not
have remote_app_name
or remote_app_data
arguments. Also, it talks in terms of peers
:
-
Relation.remote_unit_ids
maps toPeerRelation.peers_ids
-
Relation.remote_units_data
maps toPeerRelation.peers_data
Example usage
from scenario import PeerRelation
relation = PeerRelation(
endpoint="peers",
peers_data={1: {}, 2: {}, 42: {'foo': 'bar'}},
)
be mindful when using PeerRelation
not to include “this unit”'s ID in peers_data
or peers_ids
, as that would
be flagged by the Consistency Checker:
from scenario import State, PeerRelation, Context
state_in = State(relations=[
PeerRelation(
endpoint="peers",
peers_data={1: {}, 2: {}, 42: {'foo': 'bar'}},
)],
unit_id=1)
Context(...).run("start", state_in)
# invalid: this unit's id cannot be the ID of a peer.
SubordinateRelation
To declare a subordinate relation, you should use scenario.state.SubordinateRelation
. The core difference with regular relations is that subordinate relations always have exactly one remote unit (there is always exactly one primary unit that this unit can see). So unlike Relation
, a SubordinateRelation
does not have a remote_units_data
argument.
Instead, it has a remote_unit_data
taking a single Dict[str:str]
, and takes the remote unit ID as a separate argument, since there is a single remote unit (the primary or the subordinate unit).
-
Relation.remote_unit_ids
becomesSubordinateRelation.remote_unit_id
(a single ID instead of a list of IDs) -
Relation.remote_units_data
becomesSubordinateRelation.remote_unit_data
(a single databag instead of a mapping from unit IDs to databags) -
Relation.remote_app_name
remains unchanged
Example usage
from scenario.state import SubordinateRelation
relation = SubordinateRelation(
endpoint="sub",
remote_unit_data={"foo": "bar"},
remote_app_name="zookeeper",
remote_unit_id=42
)
assert relation.remote_unit_name == "zookeeper/42"