Chapter 12: Testing & Quality
Topic 71

Molecule

TestingTooling

Molecule is the standard harness for testing a role against a real, disposable target. From one command it spins up a fresh container or cloud instance, applies the role to it, verifies the result, proves idempotency, and tears the target down. It is a community project, not part of ansible-core, installed separately with pip install molecule molecule-plugins — and it is how you test the larkspur_web role against a clean Ubuntu 22.04 box instead of against your already-configured laptop.

The whole point is a throwaway target you did not set up. A green Molecule run proves the role builds the end state from nothing, because the box started with nothing — no residue from a previous run, no package the warm dev machine happened to already have. That is a stronger claim than "it ran clean here," and the rest of this topic is how Molecule makes it.

The Throwaway Target

Per run, Molecule creates an ephemeral host — a geerlingguy/docker-ubuntu2204-ansible container, a Podman container, or an EC2 instance — applies the role, and then destroys it. Because the target starts clean every time, "it works" means the role itself produces the state, not some leftover from a run you have forgotten. The disposability is the feature: a target you cannot accidentally pre-warm is a target whose result you can trust.

This is the precise gap that a green run on your laptop cannot close. Your laptop already has the package installed, the user created, the directory present — so a role that silently depends on that residue passes for you and fails on the first fresh node. Molecule removes the residue by removing the host, every single run.

The Scenario Layout

A role under Molecule gets a molecule/ directory holding one or more scenarios. The default scenario, molecule/default/, contains three files: molecule.yml (the driver, the platforms, and provisioner config), converge.yml (the playbook that applies the role under test), and verify.yml (the assertions that decide whether the converged target is right). Extra scenarios — molecule/podman/, molecule/rhel/ — test other targets or other settings alongside the default.

molecule/default/molecule.yml — driver, platform image, and verifier
driver:
  name: docker
platforms:
  - name: larkspur-web-instance
    image: geerlingguy/docker-ubuntu2204-ansible:latest
    command: /lib/systemd/systemd   # systemd-capable image so the service task is honest
    cgroupns_mode: host
    privileged: true
provisioner:
  name: ansible
verifier:
  name: ansible           # run verify.yml; testinfra is the other choice (topic 72)

The converge.yml below is the playbook Molecule applies to that target — it simply includes the role under test, exactly as a real playbook would. Keeping it minimal matters: the converge play exercises the role, nothing more, so a failure points at the role and not at scenario glue.

molecule/default/converge.yml — apply the role to the throwaway target
- name: Converge
  hosts: all
  become: true
  tasks:
    - name: Apply the larkspur_web role
      ansible.builtin.include_role:
        name: larkspur_web

The Sequence: converge / idempotence / verify / destroy

Molecule exposes the lifecycle as discrete subcommands. molecule converge applies the role to the live target. molecule idempotence runs converge a second time and fails on any changed. molecule verify runs the checks in verify.yml. molecule destroy tears the target down. Each one does exactly one thing, which is what lets you call them individually while iterating.

The combined command molecule test runs the full sequence as one gated pipeline: create, then converge, then idempotence, then verify, then destroy. Any step failing fails the whole run, and the target is destroyed at the end regardless — so molecule test is the honest gate, because it forces a clean create and a real teardown around the converge and the checks.

The molecule test lifecycle — one gated sequence
create
converge
idempotence
verify
destroy
The Molecule lifecycle — individual steps and the full gated run
# inner loop: apply once, iterate without recreating the target
molecule converge

# the second-run gate: re-converge and fail on any changed
molecule idempotence

# run the assertions in verify.yml against the converged host
molecule verify

# the full honest gate: create -> converge -> idempotence -> verify -> destroy
molecule test

Drivers

The driver decides what the target actually is. docker and podman give you fast, cheap, OS-pinned containers — the default for role unit tests, and enough to exercise the larkspur_web role's package, template, and config logic. A cloud or delegated driver stands up a real VM or instance, which you need when the role touches kernel modules, drives systemd in ways a container fakes badly, or calls real cloud APIs.

The split is honest about what each can prove. A container covers the parts of larkspur_web that are filesystem and package work; a VM covers the parts a container cannot run truthfully. A service task that is green against a container's faked init can still fail on a real host, so any role whose correctness depends on real service management wants a systemd-capable image or a VM driver, not a bare container.

Inner Loop vs Full Run

During development you run molecule converge repeatedly. It applies the role to the existing target without recreating it, so iterating on a template or a task is fast — edit, converge, look at the result, repeat, all against the same warm container. This is the inner loop, optimized for speed, and you live in it while the role is taking shape.

Before you push, you run the full molecule test, which always destroys and recreates the target. That recreation is the point: the inner loop's warm container may have accumulated state across a dozen converges, hiding a create-time bug like a missing dependency the warm box already had. The full run starts from clean and is the gate; the inner loop is for going fast, the full run for being sure.

Common Mistakes
  • Running molecule converge to iterate and never running the full molecule test, so the role is never proven against a freshly-created target and a create-time bug — a missing dependency the warm container already had — ships uncaught.
  • Testing larkspur_web only in a Docker container and assuming systemd-dependent behavior is covered, when a container fakes systemd and the service task that is green in Molecule fails on a real host; use a systemd-enabled image or a VM driver for those paths.
  • Forgetting Molecule is not ansible-core and expecting it preinstalled in an execution environment, so CI fails at molecule: command not found; it is a separate dependency that must be in the EE or the CI image.
  • Leaving molecule destroy out of the loop on a cloud driver and accumulating orphaned EC2 instances that quietly bill, because the throwaway target was never thrown away.
  • Putting the role's own dependencies only in the warm dev container and not in converge.yml or requirements.yml, so the role passes locally and fails the moment Molecule creates a clean target without them.
Best Practices
  • Give every published or shared role a molecule/default/ scenario and run the full molecule test in CI, so each role carries its own proof that it converges a clean target.
  • Match the Molecule platform image to the real target OS — Ubuntu 22.04 for the Larkspur web tier — and add scenarios for any second OS the role claims to support, since a role untested on an OS does not support it.
  • Use a systemd-capable image or a VM driver for any role whose correctness depends on real service management, rather than trusting a container's faked init.
  • Iterate with molecule converge for speed but gate the push on molecule test, which forces the create-from-clean-and-destroy cycle that catches the bugs converge alone hides.
Comparable tools Test Kitchen Chef's near-identical role-test harness Terratest stands up real infra to test Terraform, the provisioning-side analog Vagrant the older spin-up-a-VM-and-converge approach Molecule replaced for roles

Knowledge Check

Why does a green Molecule run prove more than a green run on your laptop?

  • Molecule creates a fresh throwaway target each run, so success means the role built the end state from nothing rather than relying on pre-existing residue
  • Molecule applies the role many times over in a tight loop, and the sheer number of repeated identical passes is what raises the statistical confidence in the converge result
  • Molecule disables the role's idempotency checks during the run, so a far larger share of its tasks report success
  • Molecule runs the converge with root and elevated kernel privileges that an ordinary laptop session lacks

What does molecule test run, and in what order?

  • The full sequence — create, converge, idempotence, verify, destroy — as one gated run that tears the target down at the end
  • Only converge and verify, deliberately leaving the target host running afterward for manual inspection
  • Verify first and then converge, so the assertions written in verify.yml define the desired end state that the later converge step is then obliged to reach
  • A single apply of the role to the target with no idempotency check and no teardown step at the end

When is a Docker container driver insufficient and a VM driver needed?

  • When the role's correctness depends on real service management or kernel behavior a container fakes badly, where a service task is green in the container but fails on a real host
  • Whenever the role under test installs more than one package, since a container can only manage a single package install
  • Only when testing the role on Windows targets, since any Linux distro always works correctly inside a container
  • Never, because a container can faithfully reproduce anything a full VM can, including a real init system and kernel modules, so the VM driver is a legacy option nobody actually needs anymore

Your CI job fails with molecule: command not found. What is the most likely cause?

  • Molecule is a separate community tool, not part of ansible-core, so it must be installed in the execution environment or CI image
  • The role is missing its molecule/default/ scenario directory, so the runner cannot locate the molecule command to invoke and aborts before it reaches the converge step
  • The installed ansible-core on the runner is too new, and a recent release dropped the previously bundled molecule command that older versions had once shipped on the PATH
  • The Docker container driver is unavailable on the runner, which disables the molecule binary on the PATH until a working container engine is installed first

You got correct