Chapter 12: Testing & Quality
Topic 74

Testing Collections

CollectionsTesting

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.

The three ansible-test suites a published collection runs
sanity
The static gauntlet — imports, PEP 8 on plugin code, and strict DOCUMENTATION/EXAMPLES/RETURN block validation. The bar Galaxy holds collections to.
units
pytest tests for the collection's Python — custom modules, filter and lookup plugins — in isolation, fast, no target host.
integration
Target-based tests exercising modules and roles through the collection's namespace against real or containerized hosts.

ansible-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.

The three 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 against
namespace: 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.

Common Mistakes
  • Publishing larkspur.web with Molecule on the role but no ansible-test sanity, so a malformed DOCUMENTATION block or a missing FQCN ships in a tagged release every consumer pulls.
  • Skipping ansible-test units on 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-core version, 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.yml or 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.yml manifest, the build, the namespace — is a separate thing ansible-test and a build-and-install smoke test must cover.
Best Practices
  • Run all three ansible-test suites — 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 production ansible-lint profile and full ansible-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-core versions 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.web in galaxy.yml with 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.
Comparable tools ansible-test built into 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-core versions 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-core versions 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-test refuses 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 version field in galaxy.yml sets 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