Testing Collections
A role shared by copy-paste can be loose; a collection published to a Galaxy or private Automation Hub and pulled by teams you will never meet cannot. When larkspur_web is packaged as the larkspur.web collection, the testing bar rises to match the blast radius — and the tool changes too. ansible-test, built into ansible-core, runs the sanity, unit, and integration suites the wider Ansible ecosystem uses to vet every published collection.
A consumer cannot read your role's internals before depending on it. They run ansible-galaxy collection install larkspur.web, pin a version, and trust it — so the collection's own CI is the only thing standing between a bad release and everyone who installed it. That is why a published collection carries a heavier test suite than the local role it grew from, and why the gate sits on the release rather than on a single second-run check.
Why a Collection Needs More Rigor
A local role breaks one team — a team that can read the source, see what went wrong, and fix it in place. A published larkspur.web breaks every downstream consumer at once, none of whom wrote it, all of whom pinned a version and trusted it. The asymmetry in blast radius is the whole argument: the consequence of a bad release is multiplied by every install, so the collection carries its own full test suite and gates every release on it.
This is more than a second-run idempotency check. The local role's Molecule scenario still matters, but a collection adds a packaging contract — a manifest, a build, a namespace, doc blocks — and each of those is a surface that can break a consumer independently of whether the role itself converges. The rigor scales with who is downstream.
ansible-test suites a published collection runssanityDOCUMENTATION/EXAMPLES/RETURN block validation. The bar Galaxy holds collections to.unitsintegrationansible-test Sanity
ansible-test sanity runs the ecosystem's static gauntlet: import checks, PEP 8 on any plugin code, documentation-block validation, and compatibility checks. The documentation check is strict — every module's DOCUMENTATION, EXAMPLES, and RETURN block must be present and well-formed — because those blocks are what ansible-doc renders for a consumer who cannot read your source. This is the exact bar Galaxy and certified collections are held to.
ansible-test suites, run from inside the collection tree# static gauntlet: imports, PEP 8, DOCUMENTATION/EXAMPLES/RETURN, compatibility ansible-test sanity --docker # pytest unit tests for the collection's Python — modules, filter/lookup plugins ansible-test units --docker # target-based tests exercising modules and roles against real/containerized hosts ansible-test integration --docker
ansible-test units
ansible-test units runs pytest-based unit tests for the collection's Python — custom modules, filter plugins, lookup plugins — in isolation, fast, with no target host. This is where a custom filter plugin shipped in larkspur.web gets tested as code before it is ever loaded into a play, the way you would test any Python function: feed it inputs, assert on outputs, cover the edge cases.
The payoff is where the failure surfaces. Without a unit test, a Python edge case in a filter plugin shows up as a cryptic Jinja error in a downstream user's play, far from the bug and impossible for them to debug. With one, the same edge case fails as a clean assertion in your CI, before the release is ever cut.
ansible-test integration
ansible-test integration runs target-based tests that exercise the collection's modules and roles against real or containerized hosts — the heavyweight layer that proves the packaged content works end to end. It complements the role's own Molecule scenarios rather than replacing them: Molecule proves the role converges a clean box, while collection integration tests prove the packaged module behaves correctly when invoked through the collection's namespace.
The distinction matters because packaging can break what the bare role did not. A module that works when called by short name in a local role can fail when resolved through the larkspur.web namespace, and only a test that runs the packaged collection catches it. Integration is the layer that exercises the collection as a consumer actually loads it.
Collection CI and the Release Gate
The collection repo runs ansible-test sanity, units, and integration on every pull request — often across an ansible-core version matrix — and a release is not cut unless all three are green. Versioning lives in galaxy.yml, and a changelog plus a real version bump are what let consumers pin and upgrade deliberately. That contract only works if each tagged version was actually tested, which is why the gate sits on the release tag.
galaxy.yml — the version contract a consumer pins againstnamespace: larkspur name: web version: 2.4.0 # bump on every release; the changelog says what changed readme: README.md dependencies: ansible.posix: ">=1.5.0"
Testing the collection against only one ansible-core version is the quiet trap: a behavior differs between versions, and the consumer running the version you never tested breaks on a release that passed your CI. The matrix across the ansible-core versions you claim to support is what turns "supported" from a README line into something each tagged release actually verified.
- Publishing
larkspur.webwith Molecule on the role but noansible-test sanity, so a malformedDOCUMENTATIONblock or a missing FQCN ships in a tagged release every consumer pulls. - Skipping
ansible-test unitson a custom filter plugin, so a Python edge case surfaces as a cryptic Jinja error in a downstream user's play instead of a failed unit test in your CI. - Testing the collection against only one
ansible-coreversion, then breaking consumers on the version they actually run because a behavior differed and the matrix never covered it. - Cutting a release without bumping
galaxy.ymlor writing a changelog, so consumers cannot tell what changed and cannot pin safely — a published collection's version contract is part of its test surface. - Assuming a role's local Molecule pass means the packaged collection is good, when packaging itself — the
galaxy.ymlmanifest, the build, the namespace — is a separate thingansible-testand a build-and-install smoke test must cover.
- Run all three
ansible-testsuites — sanity, units, integration — in the collection's CI and gate every release on green, because a published collection's blast radius is everyone who installs it. - Hold collection modules and plugins to the
productionansible-lintprofile and fullansible-test sanity, since the doc blocks and FQCN that are optional in a local role are contractual in a published one. - Test the collection across the
ansible-coreversions you claim to support in a matrix, and state the supported range, so a consumer's pin is a promise you actually verified. - Version
larkspur.webingalaxy.ymlwith a real changelog and only tag a release that passed the full suite, so downstream pins and upgrades rest on a tested artifact. - Add a build-and-install smoke test that packages the collection and installs the artifact, since the manifest and namespace can break independently of whether the role converges.
ansible-core, unlike Molecule which is a community add-on
terraform test the provisioning-side module-registry analog
pytest underlies the units layer
Test Kitchen + InSpec the Chef cookbook-publication equivalent
Knowledge Check
Why does a published collection need more testing rigor than the same logic as a local role?
- A local role breaks one team that can read and fix it; a published collection breaks every downstream consumer who pinned a version and can't see the source
- A packaged collection executes each of its tasks measurably slower than the identical logic would run as a plain local role, so it needs a heavier test suite purely to compensate for the overhead
- Galaxy bills the publisher a fee for every failed test in CI, so heavier up-front rigor is what keeps the cost down
- A local role cannot be tested under Molecule at all, so the published collection's suite has to make up the entire gap
What do ansible-test sanity, units, and integration each cover?
- Sanity is the static gauntlet (imports, PEP 8, doc blocks); units are hostless pytest tests of the Python; integration exercises modules and roles against real or containerized hosts
- Sanity converges the role against a live host, units tally up how many individual tasks the role contains across its files, and integration statically lints all of the collection's YAML for style problems
- They are three different names for the very same underlying check, kept around only for backward compatibility
- Sanity exercises the host's network stack, units exercise its disk, and integration exercises its memory subsystem
Why does testing across an ansible-core version matrix matter for a published collection?
- A behavior can differ between core versions, so a consumer running the version you never tested breaks on a release your single-version CI passed
- Galaxy outright refuses to accept and publish any collection whose CI tested it against fewer than five distinct
ansible-coreversions in the matrix - The matrix exists only to speed the suite up, since it fans the test jobs out to run in parallel across runners
- Older
ansible-coreversions are unable to install a published collection from Galaxy at all without the matrix
Why are galaxy.yml versioning and a changelog part of the collection's test contract?
- Consumers pin and upgrade against the version and changelog, so a release that bumps neither leaves them unable to know what changed or pin safely
ansible-testrefuses to start its sanity, unit, and integration suites at all unless a properly formatted changelog file is already present in the collection tree before the run begins- The
versionfield ingalaxy.ymlsets how many CPU cores and how much memory the test suite is allowed to consume on the CI runner during a run - A changelog is purely cosmetic documentation for the project's web page and has no real bearing on how downstream consumers decide whether to upgrade
You got correct