How to provide relation data for charm tests

This guide describes how charm interface libraries should provide sample relation data for charm unit tests. By providing test data, charm libraries prevent charms from needing to know about the underlying relation data format. Instead, charms only need to know about the library’s public API and its testing API.

As the author of an interface library, you should provide a separate testing package for your library. The testing package should expose functions that return fully populated ops.testing.Relation objects ready for use in state-transition tests.

Create a separate testing package

Your library’s testing package should be distributed separately from runtime library code. The package should be defined in a testing subdirectory, like this:

interfaces/<name>/
├── src/charmlibs/interfaces/<name>/
│   └── __init__.py
├── pyproject.toml
└── testing/
    ├── src/charmlibs/interfaces/<name>_testing/
    │   └── __init__.py
    └── pyproject.toml

Use these naming conventions:

  • Distribution package: charmlibs-interfaces-<hyphenated-interface-name>-testing

  • Import package: charmlibs.interfaces.<underscored_interface_name>_testing

Implement the required testing API

Your testing package must provide these functions:

  • relation_for_provider

  • relation_for_requirer

Both functions must follow these rules:

  1. The first argument must be endpoint, the charm’s endpoint name for this relation. This argument must be required, and must be able to be provided positionally.

  2. Any additional arguments must be optional and keyword-only. Defaults should represent a typical valid relation, not just empty databags.

  3. Return a fully formed ops.testing.Relation object.

  4. If the interface is request-response, include a response argument that defaults to True. If called with response=False, only the side of the relation that writes first should be populated.

If your interface needs helper utilities, include them in the testing package and document the intended usage.

One case where helpers are useful is if your library needs remote response data to match local request data. For example, response payloads might need to correspond to IDs, keys, or other values generated by the charm under test. In these cases, provide a testing API that makes this easy and explicit.

Test the testing package

The testing package should have its own test suites like any other package. It should only have unit tests, since the package itself only targets use in unit tests.

In the charmlibs monorepo, the testing package’s tests are run automatically in CI whenever the interface package or its testing package are changed. You can run the tests locally like this:

just unit interfaces/<interface name>/testing

Example usage in charm tests

Charms should specify the version of the library that they need in their dependencies. In their testing dependencies, they should require the library’s testing extra, but not specify any version constraints:

from charmlibs.interfaces import my_interface_testing

relation = my_interface_testing.relation_for_requirer("my-endpoint")

Tip

Charms should always depend on charmlibs-interfaces-<name>[testing], not directly on charmlibs-interfaces-<name>-testing. This ensures that the testing package version always matches the library itself.