Molecule Testing¶
What You'll Learn¶
- What Molecule does, and the stages of
molecule test - How to set up a Docker-based scenario for a role
- How to write
verify.ymlassertions that check real state, not just task status - How to test several distributions at once, and run it all in CI
Why This Exists¶
A role that "worked when I ran it manually" isn't the same as a role that's actually tested — Molecule runs a role against a disposable test instance (usually a container) as part of a repeatable pipeline, including an explicit idempotence check.
Molecule ships as part of ansible-dev-tools alongside pytest-ansible, which can expose Molecule scenarios as pytest fixtures if your project's test suite already standardizes on pytest.
Mental Model¶
A Molecule scenario is a small, disposable test lab defined in files next to your role: which instances to create, a playbook that applies the role, and a playbook that checks the result.
molecule testbuilds the lab, runs the role twice, checks the outcome, and tears everything down.
flowchart LR
A[create] --> B[prepare] --> C[converge] --> D[idempotence] --> E[verify] --> F[destroy]
| Stage | What happens | Fails when |
|---|---|---|
create |
Start test instances | Image or driver problems |
prepare |
Optional pre-setup (e.g. install python3) |
— |
converge |
Apply the role | Any task fails |
idempotence |
Apply the role again | Any task reports changed |
verify |
Run assertions | The resulting state is wrong |
destroy |
Remove instances | — |
Install¶
pip install ansible-core molecule "molecule-plugins[docker]"
docker info # Molecule's docker driver needs a working Docker daemon
Podman works too: install "molecule-plugins[podman]" and set driver: name: podman — handy on RHEL-family workstations and CI runners without a Docker daemon.
Create a Scenario¶
roles/nginx/molecule/default/
├── molecule.yml # driver and platforms
├── converge.yml # applies the role
└── verify.yml # checks the result
molecule.yml: Test on Two Distributions¶
Testing services such as nginx needs systemd inside the container, so use images built for that purpose and run them privileged with the cgroup filesystem mounted:
---
driver:
name: docker
platforms:
- name: ubuntu2404
image: geerlingguy/docker-ubuntu2404-ansible:latest
command: ""
privileged: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
pre_build_image: true
- name: rockylinux9
image: geerlingguy/docker-rockylinux9-ansible:latest
command: ""
privileged: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
pre_build_image: true
provisioner:
name: ansible
inventory:
host_vars:
ubuntu2404:
nginx_http_port: 8080
verifier:
name: ansible
These community images are convenient for testing; pin them to a digest, or build your own base images, for pipelines that must be reproducible.
converge.yml¶
---
- name: Converge
hosts: all
become: true
roles:
- role: nginx
vars:
nginx_vhosts:
- name: shop
server_names: [shop.test]
upstream: "127.0.0.1:9000"
verify.yml: Assert Real State¶
Checking that tasks didn't fail isn't enough. Verify what a user of the server would observe:
---
- name: Verify
hosts: all
become: true
gather_facts: false
tasks:
- name: Collect service state
ansible.builtin.service_facts:
- name: nginx is running and enabled
ansible.builtin.assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
- ansible_facts.services['nginx.service'].status == 'enabled'
- name: Configuration is valid
ansible.builtin.command: nginx -t
changed_when: false
- name: The shop vhost file exists
ansible.builtin.stat:
path: /etc/nginx/conf.d/shop.conf
register: vhost
- name: Vhost was rendered
ansible.builtin.assert:
that: vhost.stat.exists
- name: nginx answers on the configured port
ansible.builtin.uri:
url: "http://localhost:{{ nginx_http_port | default(80) }}/"
status_code: [200, 404, 502] # any HTTP answer proves it's listening
Day-to-Day Commands¶
molecule create # start instances, keep them
molecule converge # apply the role (repeat while developing)
molecule login -h ubuntu2404 # shell into an instance to poke around
molecule verify # run assertions
molecule idempotence # re-run and fail on any change
molecule destroy # clean up
molecule test # the full sequence, as CI runs it
molecule test --destroy never # keep instances after a failure to debug
The fast loop while writing a role is converge → verify → edit → converge, without recreating containers each time.
Reading an Idempotence Failure¶
CRITICAL Idempotence test failed because of the following tasks:
* => nginx : Download GPG key
* => nginx : Set worker_processes
Each listed task changed on the second run. Common causes and fixes are in Idempotency: a command without creates: or changed_when, get_url without a checksum, a template rendering a timestamp, or state: latest.
Additional Scenarios¶
Test variations in separate scenarios, each with its own molecule.yml and converge.yml:
molecule/
├── default/ # plain HTTP on two distributions
└── tls/ # TLS enabled, self-signed test certificate
Running in CI¶
name: Molecule
on: [pull_request]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
role: [nginx, node_exporter, app_checkout]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install ansible-core molecule "molecule-plugins[docker]"
- name: Molecule test
working-directory: roles/${{ matrix.role }}
run: molecule test
env:
PY_COLORS: "1"
ANSIBLE_FORCE_COLOR: "1"
Linting runs as its own stage (see CI/CD and Linting); recent Molecule releases no longer include lint in molecule test.
Common Mistakes¶
- Never running the
idempotencestage — the single check most likely to catch a role that silently reportschangedon every run. - Testing only the "happy path" converge and skipping
verifyassertions entirely, so a role can converge to the wrong state and still pass. - Using a plain
ubuntuimage and then failing on everyservicetask because the container has no systemd. - Testing on one distribution while declaring support for three in
meta/main.yml. - Writing verify checks that repeat the role's tasks (
file: state=directory) instead of asserting on observed state.
Interview Questions¶
- What does Molecule's
idempotencestage actually check, and why is it meaningfully different fromconvergesucceeding? - How would you test a role against both Ubuntu and RHEL in the same CI pipeline?
- What makes a good
verify.ymlassertion? - Why do service-managing roles need special container images for Molecule?
Next¶
Continue to Case Studies to see these practices applied end to end.