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.
- 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.
# Grab a small K8s charm that requires a PostgreSQL database.
git clone https://github.com/dwilding/fastapi-demo-operator.git
cd fastapi-demo-operator/
# Run the workload, using the integration tests marked 'smoke'.
uvx jjx -- -m smokeWe selected the smoke tests (the ones decorated @pytest.mark.smoke) because we want to minimize the footprint of what we're spinning up. The smoke tests deploy the charm and integrate it with a simulated PostgreSQL charm, producing containers for the workload and a database. The smoke tests don't deploy the observability apps the charm supports. Later in the demo we'll try jjx without -m smoke.
For now, open a second terminal and play with the workload:
curl http://172.17.0.2:8000/names # returns {"names":{}}
curl -X POST -d 'name=elephant' http://172.17.0.2:8000/addname/
curl http://172.17.0.2:8000/names # returns {"names":{"1":"elephant"}}You might need to use a different IP address — check the output of uvx jjx.
For more detail about the workload, see Study your application in the zero to hero K8s charm tutorial. Our charm is based on that tutorial.
Tip
You can use borescope to probe the workload:
uvx borescope --socket .jjx/socketThis gives you a prompt that feels like bash and has first-class support for Pebble commands. For example:
pebble:/# ps PID TTY TIME CMD 1 ? 00:00:00 pebble 17 ? 00:00:00 uvicorn pebble:/# services SERVICE STARTUP CURRENT fastapi enabled active
For more detail, see Command reference in the borescope docs.
Next, press Ctrl-C in your first terminal. This stops and removes all containers.
Then run the workload again, this time using the full suite of integration tests:
uvx jjxIn addition to deploying the charm and integrating it with PostgreSQL, the tests deploy COS Lite and integrate the charm with Grafana, Prometheus, and Loki. The output of uvx jjx shows how to access these apps.
Finally, open Grafana and Prometheus in your browser and explore the available data. For ideas, see Inspect the Grafana dashboard and Inspect metrics in Prometheus in the zero to hero tutorial.
In the charm dir:
uvx jjx
This runs the charm's integration tests and starts a Docker container for the workload — assuming the 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 output shows the workload's IP address. 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 = ["-m", "smoke"]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 a 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 tests try to deploy the following charms/bundles, juju starts extra containers and mocks remote units.
| Supported charm/bundle | Extra containers |
|---|---|
| postgresql-k8s | postgres |
| cos-lite | grafana, prometheus, loki |
From the perspective of the charm and its tests, everything is real. The mocked parts are Juju, the cloud, and other charms.