Molecule is the testing framework for Ansible roles — it provides a complete lifecycle for creating test instances, running the role against them, verifying the resulting state, and destroying the instances. Molecule for Ansible roles is analogous to Terratest for Terraform modules: it validates actual behaviour on real (or containerised) systems rather than just checking that the YAML syntax is valid. A role that passes 'ansible-playbook --syntax-check' may still fail to install packages, configure services incorrectly, or produce broken configuration files — only Molecule's verify step, which runs assertions against the configured system, catches these failures before the role reaches production.
Molecule's driver system supports multiple targets: Docker (fastest, cheapest, most common for local development — spins up containers in seconds), Vagrant (full VMs for closer production parity), EC2 (actual cloud instances for the most realistic testing), and Podman. The Docker driver is appropriate for most role testing because it validates the role's logic without full OS boot — services like systemd may require '--privileged' mode or workarounds for container environments. The testinfra library provides the assertion framework: Python-based tests that query the running system state and make assertions using pytest. Testinfra checks cover packages (is nginx installed and the correct version?), services (is nginx running and enabled?), files (does /etc/nginx/nginx.conf exist, is it owned by root, does it contain the expected content?), ports (is port 80 listening?), users, groups, commands and more.
# molecule/default/molecule.yml — Molecule scenario configuration
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: cricket-test-ubuntu
image: geerlingguy/docker-ubuntu2204-ansible # Ubuntu with systemd support
pre_build_image: true
privileged: true # Required for systemd in Docker
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
command: /lib/systemd/systemd # Run systemd as init
groups:
- cricket_api # Group membership for inventory
- name: cricket-test-al2023
image: geerlingguy/docker-amazonlinux2-ansible
pre_build_image: true
privileged: true
command: /sbin/init
groups:
- cricket_api
provisioner:
name: ansible
config_options:
defaults:
stdout_callback: yaml
playbooks:
converge: converge.yml # Playbook that applies the role
verify: verify.yml # Playbook that runs testinfra
prepare: prepare.yml # Optional pre-role setup
verifier:
name: ansible # Can also use: testinfra, goss
lint: | # Run linting before test
set -e
ansible-lint# molecule/default/converge.yml — Apply the role under test
---
- name: Converge
hosts: all
become: true
roles:
- role: cricket_nginx
vars:
# Test-specific overrides
nginx_upstream_port: 8080
nginx_ssl_enabled: false # Skip SSL for container test
nginx_server_name: localhost
---
# molecule/default/verify.yml — Testinfra-style assertions via Ansible
- name: Verify
hosts: all
become: true
tasks:
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert Nginx is installed
ansible.builtin.assert:
that:
- ansible_facts.packages['nginx'] is defined
fail_msg: 'Nginx package must be installed'
success_msg: 'Nginx is installed'
- name: Assert Nginx service is running and enabled
ansible.builtin.assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
- ansible_facts.services['nginx.service'].status == 'enabled'
fail_msg: 'Nginx must be running and enabled'
- name: Assert Nginx configuration file exists
ansible.builtin.stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: Verify nginx.conf attributes
ansible.builtin.assert:
that:
- nginx_conf.stat.exists
- nginx_conf.stat.mode == '0644'
- nginx_conf.stat.pw_name == 'root'
fail_msg: 'nginx.conf must exist with mode 0644 owned by root'
- name: Verify Nginx configuration is valid
ansible.builtin.command: nginx -t
changed_when: false
register: nginx_test
- name: Assert Nginx config is syntactically valid
ansible.builtin.assert:
that: nginx_test.rc == 0
fail_msg: 'nginx -t failed: {{ nginx_test.stderr }}'
- name: Verify Nginx is listening on port 80
ansible.builtin.wait_for:
port: 80
timeout: 10
- name: Verify health endpoint response
ansible.builtin.uri:
url: http://localhost/health
status_code: [200, 404] # 404 if upstream not running
timeout: 10
register: health_response
- name: Show health response
ansible.builtin.debug:
msg: 'Health endpoint returned: {{ health_response.status }}'#!/bin/bash
# Molecule lifecycle commands
# Install Molecule and drivers
pip install molecule molecule-plugins[docker] ansible-lint
# ── Full test lifecycle ───────────────────────────────────────────────────────
cd roles/cricket_nginx
# Run complete test sequence: create → prepare → converge → verify → destroy
molecule test
# Individual lifecycle commands for development
molecule create # Spin up test instances
molecule prepare # Run prepare.yml (optional pre-role setup)
molecule converge # Apply the role (converge.yml)
molecule verify # Run assertions (verify.yml)
molecule destroy # Tear down test instances
# Login to test container for debugging
molecule login --host cricket-test-ubuntu
# Run idempotency test (converge twice, check no changes on second run)
molecule test --scenario-name default 2>&1 | grep 'Idempotence\|changed'
# Test against specific scenario
molecule test --scenario-name ubuntu # Run ubuntu scenario only
molecule test --scenario-name amazonlinux
# List test instances
molecule list
# Check molecule configuration
molecule check