Skip to main content
RunBook Academy

AnsibleXXXVIII · Git Workflow and CI for AnsibleAutomation as production code

Role testing with Molecule in the pipeline

Advanced⏱ ~21 min🧪 Lab requiredansible-coremoleculepodman

What you'll learn

  • Choose which Molecule sequence a pipeline stage should run, and what each one costs
  • Explain why the idempotence step catches defect classes no static gate can reach
  • Select a test environment from what the role touches rather than from convenience
  • State what a container-based scenario cannot prove about a role that manages services

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Part XXVI covers what a Molecule scenario is, which files make it up on Molecule 26.x, and how to run one while developing a role. This lesson assumes that and asks the pipeline questions: which sequence a CI stage should run, what each stage costs in wall-clock time, and how to choose a test environment that does not manufacture confidence.

The sequence CI should run, and the ones it should not

molecule test runs the full lifecycle — destroy, create, converge, idempotence, verify, destroy — and that is the right thing for a merge gate. It is not the right thing for every stage, and the sub-commands exist because the trade-offs differ.

CommandWhat it doesWhere it belongs
molecule convergeCreates the instance if needed and applies the role, leaving it runningA developer’s inner loop. Fast, repeatable, and the instance survives for inspection.
molecule idempotenceRe-runs converge against the existing instance and fails if anything reports changedNever alone in CI — it needs a converged instance to exist first
molecule verifyRuns the scenario’s assertions against the existing instanceSame; it is a step, not a gate
molecule testThe full sequence, destroying at both endsThe merge gate. It is the only one that proves the role works from nothing.

The distinction that matters: converge proves the role works against whatever state the instance is in. test proves it works against a freshly created instance. A role that has been converged eleven times during development is being tested against a machine that already has everything it needs, and that is precisely the machine on which a missing dependency is invisible.

Why idempotence is the highest-value step

Of everything in this part, the idempotence step catches the defect class that nothing else can reach.

ansible-lint can tell you that a shell task has no changed_when. It cannot tell you whether your lineinfile regex matches the line it just wrote, whether your template renders a timestamp, or whether the file mode you set is the one the module then reports as different. Those are run-time facts about the interaction between your task and the machine’s actual state, and the only way to learn them is to run twice and compare.

The mechanism is exactly the “second run is a test” argument from Part XXVI, automated: converge, converge again, fail if the second run reports any changed.

What a failure there actually tells you is worth spelling out, because the first reaction is usually to suppress it:

  • A command or shell with no creates, removes or changed_when. It reports changed every time by contract. The fix is to describe when it really changed something, not to add changed_when: false and move on — that makes the task lie in the other direction.
  • A template that renders something non-deterministic. A timestamp, a dictionary iterated in a varying order, a value derived from ansible_date_time. The file genuinely differs on every run.
  • A regex that does not match what it wrote. lineinfile inserts a line, and next run the regexp fails to recognise it, so it inserts another. Left alone this appends a line per run indefinitely, and the idempotence test is the only thing between you and a config file with four hundred copies of the same directive.
  • Two tasks fighting. One sets a permission, another resets it. Both report changed forever, and on a real fleet this shows up as a nightly job that never reports zero changes, which is exactly the signal Part XII wants you to be able to trust.

Choosing the environment from what the role touches

On Molecule 26.x there is one driver, and it is called default:

Read-only / Safethe driver list on Molecule 26.6.0
$ molecule drivers
default

So “container driver versus VM driver” is no longer a configuration setting — it is a property of what your scenario’s create.yml actually provisions. The decision is the same one it always was; only the place you express it has moved.

Make it from what the role touches:

The role managesContainer is honestNeeds a VM
Packages and config filesYesNo
A user, a directory, file permissionsYesNo
A systemd unit it enables and startsOnly with systemd as PID 1 in the image, and even then partiallyUsually yes
Kernel parameters, modules, sysctlNo — the kernel is the host’sYes
Filesystems, LVM, mount pointsNoYes
Firewall rules, network interfaces, routingNo — the namespace is not the host’sYes
Anything requiring a rebootNoYes

Knowledge check

Knowledge check · 4 questions

  1. Q1. Why should a merge gate run molecule test rather than molecule converge?

  2. Q2. Which defects does the idempotence step catch that no static gate can? Select all that apply.

  3. Q3. On Molecule 26.x, choosing between a container and a VM is a property of what the scenario create.yml provisions rather than a driver setting.

  4. Q4. A role that manages a systemd unit passes every Molecule run in a container and fails on the first real host. What is the most likely cause, and the most useful fix?

Passing score: 75%. Answers are checked in this browser.