一键导入
jubilant-tests
Writing integration tests for charms with Jubilant + pytest-jubilant
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Writing integration tests for charms with Jubilant + pytest-jubilant
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Known-good Juju bundle shapes (COS Lite, 12-Factor + COS, Identity Platform, Charmed Kubeflow) — canonical app lists and relation edges
Implementing Juju actions for operational tasks in charms. WHEN: add Juju actions to a charm, implement backup / rotate-credentials / restore actions, declare actions in charmcraft.yaml, write action handlers, test actions with Scenario, run actions with juju run.
Adding and validating charm configuration options. WHEN: add charm configuration options, declare config in charmcraft.yaml, validate config values in config-changed, apply config to a Pebble layer, apply config to a machine charm, write Scenario tests for config.
End-to-end workflow for building ops-framework charms for custom applications (K8s and machine). WHEN: build a custom Juju charm, write src/charm.py with Pebble, write a machine charm with systemd, decide K8s vs machine substrate, scaffold a non-paas-charm with charmcraft init, customise the ops framework charm lifecycle.
Workflow for charming infrastructure software (databases, caches, message brokers, proxies, monitoring). WHEN: charm infrastructure software, charm a database / cache / message broker / proxy / monitoring system, implement peer relations, leader election, primary/replica failover, backup and restore actions, clustering.
Expert guidance for developing, building, testing, and publishing Juju charms using charmcraft. WHEN: edit charmcraft.yaml, run charmcraft pack, run charmcraft init, manage charm libraries, publish a charm to Charmhub, set up multi-base builds, configure relations / config options, set up storage or containers.
| name | jubilant-tests |
| description | Writing integration tests for charms with Jubilant + pytest-jubilant |
| globs | ["tests/integration/**"] |
Use Jubilant plus pytest-jubilant for all charm integration tests. Never use pytest-operator or python-libjuju — they are legacy approaches.
Jubilant provides a clean Python API for controlling Juju during tests; it
drives a real Juju controller, so tests exercise the full charm lifecycle.
pytest-jubilant adds the pytest plumbing — a managed juju fixture,
debug-log capture on failure, and CLI options like --juju-dump-logs.
jubilant.Juju() — the main entry point; wraps the Juju CLIjuju.deploy() — deploy a charm (local path or Charmhub name)juju.wait() — block until applications reach a target statusjuju.status() — get full model status as typed Python objectsjuju.run() — execute a command on a unitjuju.run_action() — run a Juju action and wait for resultspytest_jubilant.JujuFactory — request a second Juju (e.g. for
COS in a separate model)Pin to current stable majors in the integration dependency group:
[dependency-groups]
integration = [
"jubilant>=1.8,<2",
"pytest-jubilant>=2,<3",
]
pytest-jubilant registers itself as a pytest plugin automatically.
charm Fixturepytest-jubilant provides a module-scoped juju fixture out of the
box: it creates a temporary Juju model per test file, tears it down
afterwards, and dumps debug logs on failure. Do not write your own
juju fixture — let the plugin do it.
You only need a charm fixture so tests can deploy the packed .charm:
# tests/integration/conftest.py
import os
import pathlib
import pytest
@pytest.fixture(scope="session")
def charm():
"""Return the path of the charm under test."""
charm = os.environ.get("CHARM_PATH")
if not charm:
charm_dir = pathlib.Path() # Assume the cwd is the charm root.
charms = list(charm_dir.glob("*.charm"))
assert charms, f"No charms were found in {charm_dir.absolute()}"
assert len(charms) == 1, f"Found more than one charm: {charms}"
charm = charms[0]
path = pathlib.Path(charm).resolve()
assert path.is_file(), f"{path} is not a file"
return path
The .resolve() call matters — Jubilant rejects relative paths.
# tests/integration/test_charm.py
import jubilant
APP_NAME = "my-charm"
def test_deploy(juju: jubilant.Juju, charm):
juju.deploy(charm) # path object, not f"./{charm}"
juju.wait(jubilant.all_active, timeout=300)
status = juju.status()
assert status.apps[APP_NAME].is_active
Pass the charm path object directly to deploy — never wrap it in a
formatted string.
def test_database_integration(juju: jubilant.Juju, charm):
juju.deploy(charm)
juju.deploy("postgresql-k8s", channel="14/stable", trust=True)
juju.integrate(APP_NAME, "postgresql-k8s")
juju.wait(jubilant.all_active, timeout=10 * 60)
def test_backup_action(juju: jubilant.Juju):
task = juju.run(f"{APP_NAME}/0", "backup", {"path": "/data"})
assert task.status == "completed"
assert "backup-id" in task.results
def test_config_change(juju: jubilant.Juju):
juju.config(APP_NAME, {"log-level": "debug"})
juju.wait(jubilant.all_active, timeout=120)
When verifying observability, deploy COS Lite in a second model via
pytest_jubilant.JujuFactory, then offer the relation across the model
boundary:
import json
import jubilant
import pytest
import pytest_jubilant
import requests
@pytest.fixture(scope="module")
def cos(juju_factory: pytest_jubilant.JujuFactory):
yield juju_factory.get_juju(suffix="cos")
def test_deploy_cos(cos: jubilant.Juju):
cos.deploy("cos-lite", trust=True)
cos.wait(jubilant.all_active, timeout=10 * 60)
def test_integrate_loki(juju: jubilant.Juju, cos: jubilant.Juju):
cos.offer("loki", endpoint="logging")
juju.integrate(APP_NAME, f"{cos.model}.loki")
juju.wait(jubilant.all_active)
cos.wait(jubilant.all_active)
def test_loki_data(cos: jubilant.Juju):
"""Verify the workload's logs are landing in Loki."""
task = cos.run("traefik/0", "show-proxied-endpoints")
results = json.loads(task.results["proxied-endpoints"])
loki_url = results["loki/0"]["url"]
api = f"{loki_url}/loki/api/v1/label/juju_application/values"
response = requests.get(api, timeout=30)
response.raise_for_status()
assert APP_NAME in response.json()["data"]
The pattern — Traefik action → JSON parse → Loki/Prometheus/Tempo HTTP API check — works for any post-deploy COS smoke test.
pytest-jubilant already dumps juju debug-log on test failure. For
extra detail in CI, run with --juju-dump-logs <dir>:
tox -e integration -- --juju-dump-logs logs
Then upload the directory as a CI artefact. See the set-up-ci how-to
in the upstream ops docs for the recommended GitHub Actions wiring.
Use jubilant.all_active (and all_blocked, any_error, etc.)
instead of hand-rolling status predicates.
Set generous timeouts. Integration tests involve real infrastructure. Use at least 300 seconds for initial deploys, 600+ seconds when relating multiple applications, and 10×60 for COS Lite (the bundle takes time to settle).
Test the charm from its packed .charm file so you exercise the
full build, not just the source tree.
Remember trust=True when deploying charms that need cluster-wide
permissions (storage providers, ingress controllers, K8s cluster
bundles like cos-lite).
Don't hardcode unit names beyond <app>/0. Use juju.status() to
discover unit names dynamically when the application might scale.
juju fixture — pytest-jubilant provides
one already with model creation, teardown, and failure-time log dumping.f"./{charm}" to juju.deploy — pass the resolved
pathlib.Path directly.python-libjuju directly. Jubilant wraps the Juju CLI
and provides a cleaner API.