Migrate Harness Tests To State Transition Test
Overview
Guide charm maintainers through replacing deprecated Harness-based unit tests with deterministic state-transition tests that mirror how Juju actually fires events. Use this skill whenever a test still instantiates testing.Harness or chains multiple events inside one fixture.
When To Use
ops.testing.Harness(or Scenario Harness helpers) still appear in tests or fixtures.- Migration requires modeling relations, containers, secrets, or actions with
testing.State. - CI must stay green while Harness-only helpers (
add_relation,evaluate_status, etc.) are removed.
Preflight Checks
- Inspect
pyproject.toml(or requirements files) before touching code; ensureops[testing]is present in every dependency group that executes Scenario/state-transition tests. Remove legacyops-scenarioif it lingers. - Match the repo's package manager: use
uv add --group unit ops[testing](and mirror across groups) when[tool.uv]oruv.lockexists; otherwise edit the appropriaterequirements.txt, Poetry group, etc. - After dependency edits, re-sync the exact groups you touched (
uv sync --group unit --group lint,poetry install, etc.) so the virtualenv contains Scenario Context. - Re-run the project's bootstrap entrypoint (
uv sync,pip install -r requirements.txt,make setup, ...) to align local + CI execution before rewriting tests.
Mandatory Plan & Ledger
Create a written task list before editing files and keep it updated:
Record the migration goal (e.g., "Drop Harness from tests/unit/test_charm.py") plus all files involved.
Add explicit tasks for every preflight requirement: dependency inspection, disposable venv creation, environment sync commands, bootstrap targets.
Inventory Harness usage with the detector script, note each file's assertions, and migrate files one by one in ledger order.
Pair every code-change task with an immediate verification task (targeted pytest/tox command). Log the command, exit code, and timestamp when marking done.
If a command fails, append remediation + re-run tasks; do not advance to the next file until the paired verification succeeds.
Repository Tooling Notes
- Prefer tox over ad-hoc venvs when
tox.tomlwires environments touvrunners; it keeps CI parity. - Keep dependency groups consistent (fmt, lint, unit, integration, ...). When
ops[testing]is added to one group, mirror it across any environment that imports Scenario helpers. - Check helper targets in
Makefile/tox.tomlbefore running bespoke setup commands--they often wrap the correct bootstrap flow.
End-to-End Workflow
0. Pick the migration style
- Reuse existing fixtures/builders (for example
PenpotStateBuilder,create_state) and port their internals to Scenario primitives when available. - Inline tiny
Stateobjects only when Harness usage was minimal; large charms benefit from shared factories so relations/secrets stay consistent.
1. Inventory Harness usage
- Run
scripts/find_harness_tests.pyagainsttests/and categorize hits by surface area (actions, relations, Pebble, status). - Copy the current assertions/story so you can verify behavioral parity after rewriting.
2. Define the event boundary and states
- For each Harness test, map the single Juju event being exercised and document the expected input/output state using the migration checklist.
- Pre-populate
testing.Statewith relations, containers, config, leadership, secrets, and network data--Scenario never replays incremental Harness mutations. - Ensure collect-status side effects have the necessary mock containers/layers/service statuses.
3. Rewrite the test
- Follow state-transition-recipes.md to express one event per test.
- Invoke
ctx.run(ctx.on.<event>(...), state_in)and assert onstate_out,ctx.action_results, emitted statuses, and monkeypatched workload helpers. - Duplicate Harness failure coverage with
pytest.raises(testing.ActionFailed)or other exception checks; add separate tests for each branch instead of branching inside one function.
4. Validate and retire Harness
- Run the narrowest possible selection (e.g.,
tox -e unit -- tests/unit/test_charm.py) right after editing each file; log the result in the ledger. - Delete Harness fixtures/imports once a file is fully migrated, then rerun the detector to confirm it no longer reports hits.
- Finish with the repo's canonical unit-test target (
tox -e unit,pytest tests/unit) and only mark the migration complete when it returns 0 with zero Harness matches.
Reusable Resources
scripts/find_harness_tests.py: Recursively scans the provided paths fortesting.Harness/Scenarioreferences. Usage:python3 .github/skills/migrate-harness-tests-to-state-transition-test/scripts/find_harness_tests.py tests/unit tests/integrationreferences/migration-checklist.md: Step-by-step plan that maps every Harness helper to its state-transition counterpart and reminds you to satisfy collect-status requirements.references/state-transition-recipes.md: Ready-to-paste snippets for actions, relation-changed, Pebble-ready, update-status, and failure cases.- Canonical guide: How to migrate unit tests from Harness (authoritative reference for nuanced behaviours).
Event Playbooks (quick cues)
- Actions:
ctx.on.action("name", params=...); assert onctx.action_resultsand expected failures (testing.ActionFailed). Provide container data when_on_collect_statusinspects Pebble. - Relation changes: Reuse the same
testing.Relationinstance in bothStateandctx.on.relation_changed(...); populate remote/local databags with the "new" data that triggered the event. - Pebble / containers: Supply
testing.Containerobjects with realisticlayers,service_statuses, andcan_connectflags. Inspect the output container viastate_out.get_container(name). - Status reporting: Prefer
ctx.on.update_status()to cover_on_collect_status; create success and failure variants (inactive service, disconnected container, missing plan).
Example Prompts
- "List the files that still use
testing.Harnessso I can migrate them to state-transition tests." - "Rewrite
tests/unit/test_charm.py::test_relation_changedusing the relation recipe and show the newtesting.State." - "Explain what additional state I need to provide so
_on_collect_statusstops failing inside the new tests."
Quality Bar
- Every migrated file passes
scripts/find_harness_tests.pywith zero hits. - Each test focuses on a single Juju event with clearly documented input and output state.
- Collect-status logic has both success and failure coverage.
- The final diff removes Harness imports and helpers in addition to adding state-transition tests.
- Repository ledger shows paired code/verification tasks with recorded command outputs and remediation notes for any failures.
- Final
tox -e unit(or repo equivalent) succeeds after dependency sync and Harness removal.