jjx is an experimental test adapter for Juju charms. It lets you run a charm's workload as a Docker container, with the charm's integration tests acting like a Compose file.
To be compatible with jjx, a K8s charm must use uv to manage its dependencies and have a dependency group called integration. jjx expects the integration tests to be located at tests/integration and use Jubilant with pytest-jubilant. For more detail about the expected structure, see How to write integration tests for a charm.
What's the point of jjx?
Speed 💨
You can use jjx to quickly "run" a charm and play with its workload. No need to pack the charm or set up Juju.
And what's the catch?
The charm can't be too complex. If it requires storage, multiple units, or other charms, jjx might not work. The limitations are deliberately vague while jjx is a v0 tool.
# Grab a small K8s charm that requires a PostgreSQL database.
git clone https://github.com/canonical/operator.git
cd operator/examples/k8s-3-postgresql/
charmcraft fetch-libs
# Run the workload, mapping localhost port 5000 to workload port 8000.
uvx jjx -p 5000:8000Then play with the workload (in a second terminal):
curl localhost:5000/names # returns {"names":{}}
curl -X POST -d 'name=elephant' localhost:5000/addname/
curl localhost:5000/names # returns {"names":{"1":"elephant"}}You can also probe the workload using borescope:
uvx borescope --socket .jjx/socketThis gives you a prompt that feels like bash and has first-class support for Pebble commands. For example:
pebble:/# ls api_demo_server/ __init__.py app.py database.py pebble:/# services SERVICE STARTUP CURRENT fastapi-service enabled active
For more detail, see Command reference in the borescope docs.
- uv — To install on Ubuntu, run
sudo snap install astral-uv --classic. - Docker — To install on Ubuntu, run
sudo snap install docker.
By default, Docker commands require sudo, but jjx runs Docker commands as a regular user. To allow Docker commands, run sudo usermod -aG docker $USER, then log out and log in again.
In the charm dir:
uvx jjx
This runs the charm's integration tests and starts a Docker container for the workload — assuming the integration tests try to deploy a .charm file along with a workload image. See How jjx works.
The workload stays running until you press Ctrl-C.
The command output includes the IP address of the workload. You can play with the workload by connecting to this address.
Alternatively, to play with the workload on localhost, specify a port mapping:
uvx jjx -p <localhost-port>:<workload-port>
In the charm dir:
uvx jjx -d
This does the same thing as uvx jjx, except the command exits and the workload stays running.
To stop the workload:
uvx jjx down
jjx uses pytest to run the charm's integration tests. See How jjx works.
To set extra pytest options:
uvx jjx -- <pytest-args>
For example:
# Enable verbose logging, to show more detail about each test.
uvx jjx -- -vvTo automatically include extra pytest options, use a [tool.jjx] table in pyproject.toml. For example:
[tool.jjx]
pytest-extra-args = ["-k", "test_deploy"]The jjx Python package provides a juju command that is partially compatible with the real juju command. Running uvx jjx in the charm dir is equivalent to:
touch placeholder.charm
uv run --group integration --with jjx pytest tests/integration --no-juju-teardown
rm placeholder.charmWhen the integration tests try to deploy a .charm file along with a workload image, juju starts a container for the charm code. juju also starts a container for the workload and injects Pebble into the container. The charm code has access to the Pebble socket, as Ops expects.
Other Jubilant methods are handled by juju and routed to the charm code. For example, if an integration test calls Juju.config(), juju executes the charm code with its environment configured as a config-changed event. Ops recognizes the event and the charm code is able to apply the change using Pebble methods.
If the integration tests try to deploy postgresql-k8s, juju starts a container for the official postgres image and mocks the behavior of Charmed PostgreSQL. Once integrated with "postgresql-k8s", the charm code thinks it's talking to a real remote unit.
In other words, everything is real from the perspective of the charm and its integration tests. The mocked parts are Juju, the cloud, and other charms.