ops.testing (was: Scenario)

Install ops with the testing extra to use this API; for example: pip install ops[testing]

State-transition tests, previously known as ‘Scenario’, expect you to define the Juju state all at once, define the Juju context against which to test the charm, and fire a single event on the charm to execute its logic. The tests can then assert that the Juju state has changed as expected.

A very simple test, where the charm has no config, no integrations, the unit is the leader, and has a start handler that sets the status to active might look like this:

from ops import testing

def test_base():
    ctx = testing.Context(MyCharm)
    state = testing.State(leader=True)
    out = ctx.run(ctx.on.start(), state)
    assert out.unit_status == testing.ActiveStatus()

These ‘state-transition’ tests give charm authors a way to test how the state changes in reaction to events. They are not necessarily tests of individual methods or functions; they are testing the ‘contract’ of the charm: given a certain state, when a certain event happens, the charm should transition to another state. Unlike integration tests, they do not test using a real Juju controller and model, and focus on a single Juju unit. For simplicity, we refer to them as ‘unit’ tests.

Writing these tests should nudge you into thinking of a charm as a black-box ‘input to output’ function. The inputs are:

  • Event: why am I, the charm, being executed

  • State: am I the leader? what is my integration data? what is my config?

  • Context: what integrations can I have? what containers can I have?

The output is another State: the state after the charm has interacted with the mocked Juju model. The output state is the same type of data structure as the input state.

Transition diagram, with the input state and event on the left, the context including the charm in the centre, and the state out on the right

Writing unit tests for a charm, then, means verifying that:

  • the output state (as compared with the input state) is as expected

  • the charm does not raise uncaught exceptions while handling the event

A test consists of three broad steps:

  • Arrange:
    • declare the context

    • declare the input state

  • Act:
    • run an event (ie. obtain the output state, given the input state and the event)

  • Assert:
    • verify that the output state is what you expect it to be

    • verify that the charm has seen a certain sequence of statuses, events, and juju-log calls

Note

Unit testing is only one aspect of a comprehensive testing strategy. For more on testing charms, see Charm SDK | Testing.

class ops.testing.ActionFailed(message: str, output: ActionOutput | None = None, *, state: State | None = None)[source]

Bases: Exception

Raised when event.fail() is called during an action handler.

message: str

Optional details of the failure, as provided by ops.ActionEvent.fail().

output: ActionOutput

Any logs and results set by the Charm.

When using Context.run, both logs and results will be empty - these can be found in Context.action_logs and Context.action_results.

state: State | None

The Juju state after the action has been run.

When using Harness.run_action, this will be None.

class ops.testing.ActiveStatus(message: str = '')[source]

Bases: _EntityStatus, ActiveStatus

The unit or application is ready and active.

Set this status when the charm is correctly offering all the services it has been asked to offer. If the unit or application is operational but some feature (like high availability) is in a degraded state, set “active” with an appropriate message.

name: Literal['active'] = 'active'
class ops.testing.Address(value: str, *, hostname: str = '', cidr: str = '')[source]

Bases: _MaxPositionalArgs

An address in a Juju network space.

property address

A deprecated alias for value.

cidr: str = ''

The CIDR of the address in value.

hostname: str = ''

A host name that maps to the address in value.

value: str

The IP address in the space.

class ops.testing.BindAddress(addresses: list[Address], *, interface_name: str = '', mac_address: str | None = None)[source]

Bases: _MaxPositionalArgs

An address bound to a network interface in a Juju space.

addresses: list[Address]

The addresses in the space.

interface_name: str = ''

The name of the network interface.

mac_address: str | None = None

The MAC address of the interface.

class ops.testing.BlockedStatus(message: str = '')[source]

Bases: _EntityStatus, BlockedStatus

The unit or application requires manual intervention.

Set this status when an administrator has to manually intervene to unblock the charm to let it proceed.

name: Literal['blocked'] = 'blocked'
class ops.testing.CharmEvents[source]

Bases: object

Events generated by Juju or ops pertaining to the application lifecycle.

The events listed as attributes of this class should be accessed via the Context.on attribute. For example:

ctx.run(ctx.on.config_changed(), state)

This behaves similarly to the ops.CharmEvents class but is much simpler as there are no dynamically named attributes, and no __getattr__ version to get events. In addition, all of the attributes are methods, which are used to connect the event to the specific object that they relate to (or, for simpler events like “start” or “stop”, take no arguments).

static action(name: str, params: Mapping[str, AnyJson] | None = None, id: str | None = None)[source]

Events raised by Juju when an administrator invokes a Juju Action.

This class is the data type of events triggered when an administrator invokes a Juju Action. Callbacks bound to these events may be used for responding to the administrator’s Juju Action request.

To read the parameters for the action, see the instance variable params. To respond with the result of the action, call set_results(). To add progress messages that are visible as the action is progressing use log().

static collect_app_status()[source]

Event triggered at the end of every hook to collect app statuses for evaluation

static collect_unit_status()[source]

Event triggered at the end of every hook to collect unit statuses for evaluation

static config_changed()[source]

Event triggered when a configuration change occurs.

This event will fire in several situations:

  • When the admin reconfigures the charm using the Juju CLI, for example juju config mycharm foo=bar. This event notifies the charm of its new configuration. (The event itself, however, is not aware of what specifically has changed in the config).

  • Right after the unit starts up for the first time. This event notifies the charm of its initial configuration. Typically, this event will fire between an InstallEvent and a :class:~`ops.StartEvent` during the startup sequence (when a unit is first deployed), but in general it will fire whenever the unit is (re)started, for example after pod churn on Kubernetes, on unit rescheduling, on unit upgrade or refresh, and so on.

  • As a specific instance of the above point: when networking changes (if the machine reboots and comes up with a different IP).

  • When the app config changes, for example when juju trust is run.

Any callback method bound to this event cannot assume that the software has already been started; it should not start stopped software, but should (if appropriate) restart running software to take configuration changes into account.

static install()[source]

Event triggered when a charm is installed.

This event is triggered at the beginning of a charm’s lifecycle. Any associated callback method should be used to perform one-time setup operations, such as installing prerequisite software.

static leader_elected()[source]

Event triggered when a new leader has been elected.

Juju will trigger this event when a new leader unit is chosen for a given application.

This event fires at least once after Juju selects a leader unit. Callback methods bound to this event may take any action required for the elected unit to assert leadership. Note that only the elected leader unit will receive this event.

static pebble_check_failed(container: Container, info: CheckInfo)[source]

Event triggered when a Pebble check exceeds the configured failure threshold.

Note that the check may have started passing by the time this event is emitted (which will mean that a PebbleCheckRecoveredEvent will be emitted next). If the handler is executing code that should only be done if the check is currently failing, check the current status with event.info.status == ops.pebble.CheckStatus.DOWN.

Added in Juju version 3.6.

static pebble_check_recovered(container: Container, info: CheckInfo)[source]

Event triggered when a Pebble check recovers.

This event is only triggered when the check has previously reached a failure state (not simply failed, but failed at least as many times as the configured threshold).

Added in Juju version 3.6.

static pebble_custom_notice(container: Container, notice: Notice)[source]

Event triggered when a Pebble notice of type “custom” is created or repeats.

Added in Juju version 3.4.

static pebble_ready(container: Container)[source]

Event triggered when Pebble is ready for a workload.

This event is triggered when the Pebble process for a workload/container starts up, allowing the charm to configure how services should be launched.

Callback methods bound to this event allow the charm to run code after a workload has started its Pebble instance and is ready to receive instructions regarding what services should be started. The name prefix of the hook will depend on the container key defined in the metadata.yaml file.

static post_series_upgrade()[source]

Event triggered after a series upgrade.

This event is triggered after the administrator has done a distribution upgrade (or rolled back and kept the same series). It is called in response to juju upgrade-machine <machine> complete. Associated charm callback methods are expected to do whatever steps are necessary to reconfigure their applications for the new series. This may include things like populating the upgraded version of a database. Note however charms are expected to check if the series has actually changed or whether it was rolled back to the original series.

Scheduled for removal in Juju version 4.0.

static pre_series_upgrade()[source]

Event triggered to prepare a unit for series upgrade.

This event triggers when an administrator executes juju upgrade-machine <machine> prepare. The event will fire for each unit that is running on the specified machine. Any callback method bound to this event must prepare the charm for an upgrade to the series. This may include things like exporting database content to a version neutral format, or evacuating running instances to other machines.

It can be assumed that only after all units on a machine have executed the callback method associated with this event, the administrator will initiate steps to actually upgrade the series. After the upgrade has been completed, the PostSeriesUpgradeEvent will fire.

Scheduled for removal in Juju version 4.0.

static relation_broken(relation: RelationBase)[source]

Event triggered when a relation is removed.

If a relation is being removed (juju remove-relation or juju remove-application), once all the units have been removed, this event will fire to signal that the relationship has been fully terminated.

The event indicates that the current relation is no longer valid, and that the charm’s software must be configured as though the relation had never existed. It will only be called after every callback method bound to RelationDepartedEvent has been run. If a callback method bound to this event is being executed, it is guaranteed that no remote units are currently known locally.

static relation_changed(relation: RelationBase, *, remote_unit: int | None = None)[source]

Event triggered when relation data changes.

This event is triggered whenever there is a change to the data bucket for a related application or unit. Look at event.relation.data[event.unit/app] to see the new information, where event is the event object passed to the callback method bound to this event.

This event always fires once, after RelationJoinedEvent, and will subsequently fire whenever that remote unit changes its data for the relation. Callback methods bound to this event should be the only ones that rely on remote relation data. They should not error if the data is incomplete, since it can be guaranteed that when the remote unit or application changes its data, the event will fire again.

The data that may be queried, or set, are determined by the relation’s interface.

static relation_created(relation: RelationBase)[source]

Event triggered when a new relation is created.

This is triggered when a new integration with another app is added in Juju. This can occur before units for those applications have started. All existing relations will trigger RelationCreatedEvent before StartEvent is emitted.

static relation_departed(relation: RelationBase, *, remote_unit: int | None = None, departing_unit: int | None = None)[source]

Event triggered when a unit leaves a relation.

This is the inverse of the RelationJoinedEvent, representing when a unit is leaving the relation (the unit is being removed, the app is being removed, the relation is being removed). For remaining units, this event is emitted once for each departing unit. For departing units, this event is emitted once for each remaining unit.

Callback methods bound to this event may be used to remove all references to the departing remote unit, because there’s no guarantee that it’s still part of the system; it’s perfectly probable (although not guaranteed) that the system running that unit has already shut down.

Once all callback methods bound to this event have been run for such a relation, the unit agent will fire the RelationBrokenEvent.

static relation_joined(relation: RelationBase, *, remote_unit: int | None = None)[source]

Event triggered when a new unit joins a relation.

This event is triggered whenever a new unit of a related application joins the relation. The event fires only when that remote unit is first observed by the unit. Callback methods bound to this event may set any local unit data that can be determined using no more than the name of the joining unit and the remote private-address setting, which is always available when the relation is created and is by convention not deleted.

static remove()[source]

Event triggered when a unit is about to be terminated.

This event fires prior to Juju removing the charm and terminating its unit.

static secret_changed(secret: Secret)[source]

Event triggered on the secret observer charm when the secret owner changes its contents.

When the owner of a secret changes the secret’s contents, Juju will create a new secret revision, and all applications or units that are tracking this secret will be notified via this event that a new revision is available.

Typically, the charm will fetch the new content by calling event.secret.get_content() with refresh=True to tell Juju to start tracking the new revision.

Added in Juju version 3.0: Charm secrets added in Juju 3.0, user secrets added in Juju 3.3

static secret_expired(secret: Secret, *, revision: int)[source]

Event triggered on the secret owner charm when a secret’s expiration time elapses.

This event is fired on the secret owner to inform it that the secret revision must be removed. The event will keep firing until the owner removes the revision by calling event.secret.remove_revision().

Added in Juju version 3.0.

static secret_remove(secret: Secret, *, revision: int)[source]

Event triggered on the secret owner charm when a secret revision can be removed.

When the owner of a secret creates a new revision, and after all observers have updated to that new revision, this event will be fired to inform the secret owner that the old revision can be removed.

After any required cleanup, the charm should call event.secret.remove_revision() to remove the now-unused revision. If the charm does not, then the event will be emitted again, when further revisions are ready for removal.

Added in Juju version 3.0.

static secret_rotate(secret: Secret)[source]

Event triggered on the secret owner charm when the secret’s rotation policy elapses.

This event is fired on the secret owner to inform it that the secret must be rotated. The event will keep firing until the owner creates a new revision by calling event.secret.set_content().

Added in Juju version 3.0.

static start()[source]

Event triggered immediately after first configuration change.

This event is triggered immediately after the first ConfigChangedEvent. Callback methods bound to the event should be used to ensure that the charm’s software is in a running state. Note that the charm’s software should be configured so as to persist in this state through reboots without further intervention on Juju’s part.

static stop()[source]

Event triggered when a charm is shut down.

This event is triggered when an application’s removal is requested by the client. The event fires immediately before the end of the unit’s destruction sequence. Callback methods bound to this event should be used to ensure that the charm’s software is not running, and that it will not start again on reboot.

static storage_attached(storage: Storage)[source]

Event triggered when new storage becomes available.

This event is triggered when new storage is available for the charm to use.

Callback methods bound to this event allow the charm to run code when storage has been added. Such methods will be run before the InstallEvent fires, so that the installation routine may use the storage. The name prefix of this hook will depend on the storage key defined in the metadata.yaml file.

static storage_detaching(storage: Storage)[source]

Event triggered prior to removal of storage.

This event is triggered when storage a charm has been using is going away.

Callback methods bound to this event allow the charm to run code before storage is removed. Such methods will be run before storage is detached, and always before the StopEvent fires, thereby allowing the charm to gracefully release resources before they are removed and before the unit terminates. The name prefix of the hook will depend on the storage key defined in the metadata.yaml file.

static update_status()[source]

Event triggered by a status update request from Juju.

This event is periodically triggered by Juju so that it can provide constant feedback to the administrator about the status of the application the charm is modeling. Any callback method bound to this event should determine the “health” of the application and set the status appropriately.

The interval between UpdateStatusEvent events can be configured model-wide, e.g. juju model-config update-status-hook-interval=1m.

static upgrade_charm()[source]

Event triggered by request to upgrade the charm.

This event will be triggered when an administrator executes juju upgrade-charm. The event fires after Juju has unpacked the upgraded charm code, and so this event will be handled by the callback method bound to the event in the new codebase. The associated callback method is invoked provided there is no existing error state. The callback method should be used to reconcile current state written by an older version of the charm into whatever form that is needed by the current charm version.

class ops.testing.CheckInfo(name: str, *, level: pebble.CheckLevel | None = None, status: pebble.CheckStatus = CheckStatus.UP, failures: int = 0, threshold: int = 3)[source]

Bases: _MaxPositionalArgs

A health check for a Pebble workload container.

failures: int = 0

Number of failures since the check last succeeded.

level: CheckLevel | None = None

Level of the check.

name: str

Name of the check.

status: CheckStatus = 'up'

Status of the check.

ops.pebble.CheckStatus.UP means the check is healthy (the number of failures is fewer than the threshold), ops.pebble.CheckStatus.DOWN means the check is unhealthy (the number of failures has reached the threshold).

threshold: int = 3

Failure threshold.

This is how many consecutive failures for the check to be considered ‘down’.

class ops.testing.CloudCredential(*, auth_type: str, attributes: dict[str, str] = <factory>, redacted: list[str] = <factory>)[source]

Bases: _MaxPositionalArgs

Credentials for cloud.

Used as the type of attribute credential in CloudSpec.

attributes: dict[str, str]

A dictionary containing cloud credentials. For example, for AWS, it contains access-key and secret-key; for Azure, application-id, application-password and subscription-id can be found here.

auth_type: str

Authentication type.

redacted: list[str]

A list of redacted generic cloud API secrets.

class ops.testing.CloudSpec(type: str, *, name: str = 'localhost', region: str | None = None, endpoint: str | None = None, identity_endpoint: str | None = None, storage_endpoint: str | None = None, credential: CloudCredential | None = None, ca_certificates: list[str] = <factory>, skip_tls_verify: bool = False, is_controller_cloud: bool = False)[source]

Bases: _MaxPositionalArgs

Cloud specification information (metadata) including credentials.

ca_certificates: list[str]

A list of CA certificates.

credential: CloudCredential | None = None

Cloud credentials with key-value attributes.

endpoint: str | None = None

Endpoint of the cloud.

identity_endpoint: str | None = None

Identity endpoint of the cloud.

is_controller_cloud: bool = False

If this is the cloud used by the controller.

name: str = 'localhost'

Juju cloud name.

region: str | None = None

Region of the cloud.

skip_tls_verify: bool = False

Whether to skip TLS verification.

storage_endpoint: str | None = None

Storage endpoint of the cloud.

type: str

Type of the cloud.

class ops.testing.Container(name: str, *, can_connect: bool = False, _base_plan: dict[str, Any] = <factory>, layers: dict[str, pebble.Layer] = <factory>, service_statuses: dict[str, pebble.ServiceStatus] = <factory>, mounts: dict[str, Mount] = <factory>, execs: Iterable[Exec] = frozenset({}), notices: list[Notice] = <factory>, check_infos: frozenset[CheckInfo] = frozenset({}))[source]

Bases: _MaxPositionalArgs

A Kubernetes container where a charm’s workload runs.

can_connect: bool = False

When False, all Pebble operations will fail.

check_infos: frozenset[CheckInfo] = frozenset({})

All Pebble health checks that have been added to the container.

execs: Iterable[Exec] = frozenset({})

Simulate executing commands in the container.

Specify each command the charm might run in the container and an Exec containing its return code and any stdout/stderr.

For example:

container = Container(
    name='foo',
    execs={
        Exec(['whoami'], return_code=0, stdout='ubuntu'),
        Exec(
            ['dig', '+short', 'canonical.com'],
            return_code=0,
            stdout='185.125.190.20\n185.125.190.21',
        ),
    }
)
get_filesystem(ctx: Context) pathlib.Path[source]

Simulated Pebble filesystem in this context.

Returns:

A temporary filesystem containing any files or directories the charm pushed to the container.

layers: dict[str, Layer]

All ops.pebble.Layer definitions that have already been added to the container.

mounts: dict[str, Mount]

Provides access to the contents of the simulated container filesystem.

For example, suppose you want to express that your container has:

  • /home/foo/bar.py

  • /bin/bash

  • /bin/baz

this becomes:

mounts = {
    'foo': Mount('/home/foo', pathlib.Path('/path/to/local/dir/containing/bar/py/')),
    'bin': Mount('/bin/', pathlib.Path('/path/to/local/dir/containing/bash/and/baz/')),
}
name: str

Name of the container, as found in the charm metadata.

notices: list[Notice]

Any Pebble notices that already exist in the container.

property plan: Plan

The ‘computed’ Pebble plan.

This is the base plan plus the layers that have been added on top. You should run your assertions on this plan, not so much on the layers, as those are input data.

service_statuses: dict[str, ServiceStatus]

The current status of each Pebble service running in the container.

property services: dict[str, ServiceInfo]

The Pebble services as rendered in the plan.

class ops.testing.Context(charm_type: type[CharmType], meta: dict[str, Any] | None = None, *, actions: dict[str, Any] | None = None, config: dict[str, Any] | None = None, charm_root: str | Path | None = None, juju_version: str = '3.5', capture_deferred_events: bool = False, capture_framework_events: bool = False, app_name: str | None = None, unit_id: int | None = 0, app_trusted: bool = False)[source]

Bases: Generic[CharmType]

Represents a simulated charm’s execution context.

The main entry point to running a test. It contains:

  • the charm source code being executed

  • the metadata files associated with it

  • a charm project repository root

  • the Juju version to be simulated

After you have instantiated Context, typically you will call run() to execute the charm once, write any assertions you like on the output state returned by the call, write any assertions you like on the Context attributes, then discard the Context.

Each Context instance is in principle designed to be single-use: Context is not cleaned up automatically between charm runs.

Any side effects generated by executing the charm, that are not rightful part of the State, are in fact stored in the Context:

This allows you to write assertions not only on the output state, but also, to some extent, on the path the charm took to get there.

A typical test will look like:

from charm import MyCharm, MyCustomEvent  # noqa

def test_foo():
    # Arrange: set the context up
    ctx = Context(MyCharm)
    # Act: prepare the state and emit an event
    state_out = ctx.run(ctx.on.update_status(), State())
    # Assert: verify the output state is what you think it should be
    assert state_out.unit_status == ActiveStatus('foobar')
    # Assert: verify the Context contains what you think it should
    assert len(c.emitted_events) == 4
    assert isinstance(c.emitted_events[3], MyCustomEvent)

If you need access to the charm object that will handle the event, use the class in a with statement, like:

def test_foo():
    ctx = Context(MyCharm)
    with ctx(ctx.on.start(), State()) as manager:
        manager.charm._some_private_setup()
        manager.run()
__call__(event: _Event, state: State) Manager[CharmType][source]

Context manager to introspect live charm object before and after the event is emitted.

Usage:

ctx = Context(MyCharm)
with ctx(ctx.on.start(), State()) as manager:
    manager.charm._some_private_setup()
    manager.run()  # this will fire the event
    assert manager.charm._some_private_attribute == "bar"  # noqa
Parameters:
  • event – the event that the charm will respond to.

  • state – the State instance to use when handling the event.

action_logs: list[str]

The logs associated with the action output, set by the charm with ops.ActionEvent.log()

This will be empty when handling a non-action event.

action_results: dict[str, Any] | None

A key-value mapping assigned by the charm as a result of the action.

This will be None if the charm never calls ops.ActionEvent.set_results()

app_status_history: list[_EntityStatus]

A record of the app statuses the charm has set

emitted_events: list[ops.EventBase]

A record of the events (including custom) that the charm has processed

juju_log: list[JujuLogLine]

A record of what the charm has sent to juju-log

on: CharmEvents

The events that this charm can respond to.

Use this when calling run() to specify the event to emit.

removed_secret_revisions: list[int]

A record of the secret revisions the charm has removed

requested_storages: dict[str, int]

A record of the storages the charm has requested

run(event: _Event, state: State) State[source]

Trigger a charm execution with an event and a State.

Calling this function will call ops.main and set up the context according to the specified State, then emit the event on the charm.

Parameters:
  • event – the event that the charm will respond to. Use the on attribute to specify the event; for example: ctx.on.start().

  • state – the State instance to use as data source for the hook tool calls that the charm will invoke when handling the event.

run_action(action: str, state: State)[source]

Use run() instead.

Private:

unit_status_history: list[_EntityStatus]

A record of the unit statuses the charm has set

workload_version_history: list[str]

A record of the workload versions the charm has set

class ops.testing.DeferredEvent(handle_path: str, owner: str, observer: str, snapshot_data: dict[~typing.Any, ~typing.Any] = <factory>)[source]

Bases: object

An event that has been deferred to run prior to the next Juju event.

Tests should not instantiate this class directly: use the deferred method of the event instead. For example:

ctx = Context(MyCharm) deferred_start = ctx.on.start().deferred(handler=MyCharm._on_start) state = State(deferred=[deferred_start])

handle_path: str
property name

A comparable name for the event.

observer: str
owner: str
snapshot_data: dict[Any, Any]
class ops.testing.ErrorStatus(message: str = '')[source]

Bases: _EntityStatus, ErrorStatus

The unit status is error.

The unit-agent has encountered an error (the application or unit requires human intervention in order to operate correctly).

This status is read-only; trying to set unit or application status to ErrorStatus will raise InvalidStatusError.

name: Literal['error'] = 'error'
class ops.testing.Exec(command_prefix: Sequence[str], *, return_code: int = 0, stdout: str = '', stderr: str = '', _change_id: int = <factory>)[source]

Bases: _MaxPositionalArgs

Mock data for simulated ops.Container.exec() calls.

command_prefix: Sequence[str]
return_code: int = 0

The return code of the process.

Use 0 to mock the process ending successfully, and other values for failure.

stderr: str = ''

Any content written to stderr by the process.

Provide content that the real process would write to stderr, which can be read by the charm.

stdout: str = ''

Any content written to stdout by the process.

Provide content that the real process would write to stdout, which can be read by the charm.

class ops.testing.ICMPPort(*, port: int | None = None, protocol: _RawPortProtocolLiteral = 'icmp', _max_positional_args: Final = 0)[source]

Bases: Port

Represents an ICMP port on the charm host.

class ops.testing.JujuLogLine(level: str, message: str)[source]

Bases: object

An entry in the Juju debug-log.

level: str

The level of the message, for example INFO or ERROR.

message: str

The log message.

class ops.testing.MaintenanceStatus(message: str = '')[source]

Bases: _EntityStatus, MaintenanceStatus

The unit or application is performing maintenance tasks.

Set this status when the charm is performing an operation such as apt install, or is waiting for something under its control, such as pebble-ready or an exec operation in the workload container. In contrast to WaitingStatus, “maintenance” reflects activity on this unit or charm, not on peers or related units.

name: Literal['maintenance'] = 'maintenance'
class ops.testing.Manager(ctx: Context[CharmType], arg: _Event, state_in: State)[source]

Bases: Generic[CharmType]

Context manager to offer test code some runtime charm object introspection.

This class should not be instantiated directly: use a Context in a with statement instead, for example:

ctx = Context(MyCharm)
with ctx(ctx.on.start(), State()) as manager:
    manager.charm.setup()
    manager.run()
property charm: CharmType

The charm object instantiated by ops to handle the event.

The charm is only available during the context manager scope.

run() State[source]

Emit the event and proceed with charm execution.

This can only be done once.

class ops.testing.Model(name: str = <factory>, *, uuid: str = <factory>, type: Literal['kubernetes', 'lxd'] = 'kubernetes', cloud_spec: CloudSpec | None = None)[source]

Bases: _MaxPositionalArgs

The Juju model in which the charm is deployed.

cloud_spec: CloudSpec | None = None

Cloud specification information (metadata) including credentials.

name: str

The name of the model.

type: Literal['kubernetes', 'lxd'] = 'kubernetes'

The type of Juju model.

uuid: str

A unique identifier for the model, typically generated by Juju.

class ops.testing.Mount(*, location: str | pathlib.PurePosixPath, source: str | pathlib.Path)[source]

Bases: _MaxPositionalArgs

Maps local files to a Container filesystem.

location: str | PurePosixPath

The location inside of the container.

source: str | Path

The content to provide when the charm does ops.Container.pull().

class ops.testing.Network(binding_name: str, bind_addresses: list[BindAddress] = <factory>, *, ingress_addresses: list[str] = <factory>, egress_subnets: list[str] = <factory>)[source]

Bases: _MaxPositionalArgs

A Juju network space.

bind_addresses: list[BindAddress]

Addresses that the charm’s application should bind to.

binding_name: str

The name of the network space.

egress_subnets: list[str]

Subnets that other units will see the charm connecting from.

ingress_addresses: list[str]

Addresses other applications should use to connect to the unit.

class ops.testing.Notice(key: str, *, id: str = <factory>, user_id: int | None = None, type: pebble.NoticeType | str = NoticeType.CUSTOM, first_occurred: datetime.datetime = <factory>, last_occurred: datetime.datetime = <factory>, last_repeated: datetime.datetime = <factory>, occurrences: int = 1, last_data: dict[str, str] = <factory>, repeat_after: datetime.timedelta | None = None, expire_after: datetime.timedelta | None = None)[source]

Bases: _MaxPositionalArgs

A Pebble notice.

expire_after: timedelta | None = None

How long since one of these last occurred until Pebble will drop the notice.

first_occurred: datetime

The first time one of these notices (type and key combination) occurs.

id: str

Unique ID for this notice.

key: str

The notice key, a string that differentiates notices of this type.

This is in the format domain/path; for example: canonical.com/postgresql/backup or example.com/mycharm/notice.

last_data: dict[str, str]

Additional data captured from the last occurrence of one of these notices.

last_occurred: datetime

The last time one of these notices occurred.

last_repeated: datetime

The time this notice was last repeated.

See Pebble’s Notices documentation for an explanation of what “repeated” means.

occurrences: int = 1

The number of times one of these notices has occurred.

repeat_after: timedelta | None = None

Minimum time after one of these was last repeated before Pebble will repeat it again.

type: NoticeType | str = 'custom'

Type of the notice.

user_id: int | None = None

UID of the user who may view this notice (None means notice is public).

class ops.testing.PeerRelation(endpoint: str, interface: str | None = None, *, id: int = <factory>, local_app_data: RawDataBagContents = <factory>, local_unit_data: RawDataBagContents = <factory>, peers_data: dict[UnitID, RawDataBagContents] = <factory>)[source]

Bases: RelationBase

A relation to share data between units of the charm.

peers_data: dict[int, Dict[str, str]]

Current contents of the peer databags.

class ops.testing.Port(port: int | None = None, *, protocol: _RawPortProtocolLiteral = 'tcp')[source]

Bases: _MaxPositionalArgs

Represents a port on the charm host.

Port objects should not be instantiated directly: use TCPPort, UDPPort, or ICMPPort instead.

port: int | None = None

The port to open. Required for TCP and UDP; not allowed for ICMP.

protocol: Literal['tcp', 'udp', 'icmp'] = 'tcp'

The protocol that data transferred over the port will use.

class ops.testing.Relation(endpoint: str, interface: str | None = None, *, id: int = <factory>, local_app_data: RawDataBagContents = <factory>, local_unit_data: RawDataBagContents = <factory>, remote_app_name: str = 'remote', limit: int = 1, remote_app_data: RawDataBagContents = <factory>, remote_units_data: dict[UnitID, RawDataBagContents] = <factory>)[source]

Bases: RelationBase

An integration between the charm and another application.

limit: int = 1

The maximum number of integrations on this endpoint.

remote_app_data: Dict[str, str]

The current content of the application databag.

remote_app_name: str = 'remote'

The name of the remote application, as in the charm’s metadata.

remote_units_data: dict[int, Dict[str, str]]

The current content of the databag for each unit in the relation.

class ops.testing.RelationBase(endpoint: str, interface: str | None = None, *, id: int = <factory>, local_app_data: RawDataBagContents = <factory>, local_unit_data: RawDataBagContents = <factory>)[source]

Bases: _MaxPositionalArgs

Base class for the various types of integration (relation).

endpoint: str

Relation endpoint name. Must match some endpoint name defined in the metadata.

id: int

Juju relation ID. Every new Relation instance gets a unique one, if there’s trouble, override.

interface: str | None = None

Interface name. Must match the interface name attached to this endpoint in the metadata. If left empty, it will be automatically derived from the metadata.

local_app_data: Dict[str, str]

This application’s databag for this relation.

local_unit_data: Dict[str, str]

This unit’s databag for this relation.

property relation_id: NoReturn

Use .id instead of .relation_id.

Private:

class ops.testing.Resource(*, name: str, path: str | pathlib.Path)[source]

Bases: _MaxPositionalArgs

Represents a resource made available to the charm.

name: str

The name of the resource, as found in the charm metadata.

path: str | Path

A local path that will be provided to the charm as the content of the resource.

class ops.testing.Secret(tracked_content: RawSecretRevisionContents, *, latest_content: RawSecretRevisionContents | None = None, id: str = <factory>, owner: Literal['unit', 'app', None] = None, remote_grants: dict[int, set[str]] = <factory>, label: str | None = None, description: str | None = None, expire: datetime.datetime | None = None, rotate: SecretRotate | None = None, _tracked_revision: int = 1, _latest_revision: int = 1)[source]

Bases: _MaxPositionalArgs

A Juju secret.

This class is used for both user and charm secrets.

description: str | None = None

A human-readable description of the secret.

expire: datetime | None = None

The time at which the secret will expire.

id: str

The Juju ID of the secret.

This is automatically assigned and should not usually need to be explicitly set.

label: str | None = None

A human-readable label the charm can use to retrieve the secret.

If this is set, it implies that the charm has previously set the label.

latest_content: Dict[str, str] | None = None

The content of the latest revision of the secret.

This is the content the charm will receive with a ops.Secret.peek_content() call.

owner: Literal['unit', 'app', None] = None

Indicates if the secret is owned by this unit, this application, or another application/unit.

If None, the implication is that read access to the secret has been granted to this unit.

remote_grants: dict[int, set[str]]

Mapping from relation IDs to remote units and applications to which this secret has been granted.

rotate: SecretRotate | None = None

The rotation policy for the secret.

tracked_content: Dict[str, str]

The content of the secret that the charm is currently tracking.

This is the content the charm will receive with a ops.Secret.get_content() call.

class ops.testing.State(*, config: dict[str, str | int | float | bool] = <factory>, relations: Iterable[RelationBase] = <factory>, networks: Iterable[Network] = <factory>, containers: Iterable[Container] = <factory>, storages: Iterable[Storage] = <factory>, opened_ports: Iterable[Port] = <factory>, leader: bool = False, model: Model = Model(name='04d5vC3YGYmwqpONWQIN', uuid='af83f61e-b5fe-4140-84a8-b3ba579b537c', type='kubernetes', cloud_spec=None), secrets: Iterable[Secret] = <factory>, resources: Iterable[Resource] = <factory>, planned_units: int = 1, deferred: list[DeferredEvent] = <factory>, stored_states: Iterable[StoredState] = <factory>, app_status: _EntityStatus = UnknownStatus(), unit_status: _EntityStatus = UnknownStatus(), workload_version: str = '')[source]

Bases: _MaxPositionalArgs

Represents the Juju-owned portion of a unit’s state.

Roughly speaking, it wraps all hook-tool- and pebble-mediated data a charm can access in its lifecycle. For example, status-get will return data from State.unit_status, is-leader will return data from State.leader, and so on.

app_status: _EntityStatus = UnknownStatus()

Status of the application.

config: dict[str, str | int | float | bool]

The present configuration of this charm.

containers: Iterable[Container]

All containers (whether they can connect or not) that this charm is aware of.

deferred: list[DeferredEvent]

Events that have been deferred on this charm by some previous execution.

get_container(container: str, /) Container[source]

Get container from this State, based on its name.

get_network(binding_name: str, /) Network[source]

Get network from this State, based on its binding name.

get_relation(relation: int, /) RelationBase[source]

Get relation from this State, based on the relation’s id.

get_relations(endpoint: str) tuple[RelationBase, ...][source]

Get all relations on this endpoint from the current state.

get_secret(*, id: str | None = None, label: str | None = None) Secret[source]

Get secret from this State, based on the secret’s id or label.

get_storage(storage: str, /, *, index: int | None = 0) Storage[source]

Get storage from this State, based on the storage’s name and index.

get_stored_state(stored_state: str, /, *, owner_path: str | None = None) StoredState[source]

Get stored state from this State, based on the stored state’s name and owner_path.

leader: bool = False

Whether this charm has leadership.

model: Model = Model(name='04d5vC3YGYmwqpONWQIN', uuid='af83f61e-b5fe-4140-84a8-b3ba579b537c', type='kubernetes', cloud_spec=None)

The model this charm lives in.

networks: Iterable[Network]

Manual overrides for any relation and extra bindings currently provisioned for this charm. If a metadata-defined relation endpoint is not explicitly mapped to a Network in this field, it will be defaulted.

Warning

extra-bindings is a deprecated, regretful feature in Juju/ops. For completeness we support it, but use at your own risk. If a metadata-defined extra-binding is left empty, it will be defaulted.

opened_ports: Iterable[Port]

Ports opened by Juju on this charm.

planned_units: int = 1

Number of non-dying planned units that are expected to be running this application.

Use with caution.

relations: Iterable[RelationBase]

All relations that currently exist for this charm.

resources: Iterable[Resource]

All resources that this charm can access.

secrets: Iterable[Secret]

The secrets this charm has access to (as an owner, or as a grantee).

The presence of a secret in this list entails that the charm can read it. Whether it can manage it or not depends on the individual secret’s owner flag.

storages: Iterable[Storage]

All attached storage instances for this charm.

If a storage is not attached, omit it from this listing.

stored_states: Iterable[StoredState]

Contents of a charm’s stored state.

unit_status: _EntityStatus = UnknownStatus()

Status of the unit.

workload_version: str = ''

Workload version.

class ops.testing.Storage(name: str, *, index: int = <factory>)[source]

Bases: _MaxPositionalArgs

Represents an (attached) storage made available to the charm container.

get_filesystem(ctx: Context) pathlib.Path[source]

Simulated filesystem root in this context.

index: int

The index of this storage instance.

For Kubernetes charms, this will always be 1. For machine charms, each new Storage instance gets a new index.

name: str

The name of the storage, as found in the charm metadata.

class ops.testing.StoredState(name: str = '_stored', *, owner_path: str | None = None, content: dict[str, Any] = <factory>, _data_type_name: str = 'StoredStateData')[source]

Bases: _MaxPositionalArgs

Represents unit-local state that persists across events.

content: dict[str, Any]

The content of the ops.StoredState instance.

name: str = '_stored'

The attribute in the parent Object where the state is stored.

For example, _stored in this class:

class MyCharm(ops.CharmBase):
    _stored = ops.StoredState()
owner_path: str | None = None

The path to the owner of this StoredState instance.

If None, the owner is the Framework. Otherwise, /-separated object names, for example MyCharm/MyCharmLib.

class ops.testing.SubordinateRelation(endpoint: str, interface: str | None = None, *, id: int = <factory>, local_app_data: RawDataBagContents = <factory>, local_unit_data: RawDataBagContents = <factory>, remote_app_data: RawDataBagContents = <factory>, remote_unit_data: RawDataBagContents = <factory>, remote_app_name: str = 'remote', remote_unit_id: int = 0)[source]

Bases: RelationBase

A relation to share data between a subordinate and a principal charm.

remote_app_data: Dict[str, str]

The current content of the remote application databag.

remote_app_name: str = 'remote'

The name of the remote application that this unit is attached to.

remote_unit_data: Dict[str, str]

The current content of the remote unit databag.

remote_unit_id: int = 0

The ID of the remote unit that this unit is attached to.

property remote_unit_name: str

The full name of the remote unit, in the form remote/0.

class ops.testing.TCPPort(port: int = None, *, protocol: _RawPortProtocolLiteral = 'tcp')[source]

Bases: Port

Represents a TCP port on the charm host.

class ops.testing.UDPPort(port: int = None, *, protocol: _RawPortProtocolLiteral = 'udp')[source]

Bases: Port

Represents a UDP port on the charm host.

class ops.testing.UnknownStatus[source]

Bases: _EntityStatus, UnknownStatus

The unit status is unknown.

A unit-agent has finished calling install, config-changed and start, but the charm has not called status-set yet.

This status is read-only; trying to set unit or application status to UnknownStatus will raise InvalidStatusError.

name: Literal['unknown'] = 'unknown'
class ops.testing.WaitingStatus(message: str = '')[source]

Bases: _EntityStatus, WaitingStatus

The unit or application is waiting on a charm it’s integrated with.

Set this status when waiting on a charm this is integrated with. For example, a web app charm would set “waiting” status when it is integrated with a database charm that is not ready yet (it might be creating a database). In contrast to MaintenanceStatus, “waiting” reflects activity on related units, not on this unit or charm.

name: Literal['waiting'] = 'waiting'
class ops.testing.errors.ContextSetupError

Bases: RuntimeError

Raised by Context when setup fails.

class ops.testing.errors.AlreadyEmittedError

Bases: RuntimeError

Raised when run() is called more than once.

class ops.testing.errors.ScenarioRuntimeError

Bases: RuntimeError

Base class for exceptions raised by the runtime module.

class ops.testing.errors.UncaughtCharmError

Bases: ScenarioRuntimeError

Error raised if the charm raises while handling the event being dispatched.

class ops.testing.errors.InconsistentScenarioError

Bases: ScenarioRuntimeError

Error raised when the combination of state and event is inconsistent.

class ops.testing.errors.StateValidationError

Bases: RuntimeError

Raised when individual parts of the State are inconsistent.

class ops.testing.errors.MetadataNotFoundError

Bases: RuntimeError

Raised when a metadata file can’t be found in the provided charm root.

class ops.testing.errors.ActionMissingFromContextError

Bases: Exception

Raised when the user attempts to invoke action hook tools outside an action context.

class ops.testing.errors.NoObserverError

Bases: RuntimeError

Error raised when the event being dispatched has no registered observers.

class ops.testing.errors.BadOwnerPath

Bases: RuntimeError

Error raised when the owner path does not lead to a valid ObjectEvents instance.