Ansible Molecule Testing — Write and Run Role Tests
Introduction
Molecule is the standard testing framework for Ansible roles. It creates temporary instances (Docker containers, VMs, or cloud instances), runs your role against them, verifies the result, and tears everything down. This guide covers setup, writing tests, multiple scenarios, and CI/CD integration.
Installation
# Install Molecule with Docker driver
pip install molecule molecule-plugins[docker]
# Or with Podman
pip install molecule molecule-plugins[podman]
# Verify installation
molecule --version
Initialize a Role with Molecule
# Create a new role with Molecule scaffolding
molecule init role my_nginx --driver-name docker
# Or add Molecule to an existing role
cd roles/my_nginx
molecule init scenario --driver-name docker
Directory structure:
roles/my_nginx/
├── defaults/
│ └── main.yml
├── handlers/
│ └── main.yml
├── molecule/
│ └── default/
│ ├── converge.yml
│ ├── molecule.yml
│ ├── verify.yml
│ └── prepare.yml
├── tasks/
│ └── main.yml
└── templates/
molecule.yml Configuration
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-instance
image: ubuntu:24.04
pre_build_image: true
command: /bin/bash
tmpfs:
- /run
- /tmp
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
privileged: true
- name: rocky-instance
image: rockylinux:9
pre_build_image: true
command: /usr/sbin/init
privileged: true
provisioner:
name: ansible
config_options:
defaults:
callbacks_enabled: profile_tasks
inventory:
host_vars:
ubuntu-instance:
ansible_python_interpreter: /usr/bin/python3
verifier:
name: ansible
converge.yml — The Playbook
# molecule/default/converge.yml
---
- name: Converge
hosts: all
become: true
roles:
- role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
vars:
nginx_port: 8080
nginx_server_name: test.example.com
verify.yml — Assertions
# molecule/default/verify.yml
---
- name: Verify
hosts: all
become: true
tasks:
- name: Check Nginx is installed
ansible.builtin.package:
name: nginx
state: present
check_mode: true
register: nginx_pkg
failed_when: nginx_pkg.changed
- name: Check Nginx is running
ansible.builtin.service:
name: nginx
state: started
check_mode: true
register: nginx_svc
failed_when: nginx_svc.changed
- name: Check Nginx is listening on configured port
ansible.builtin.wait_for:
port: 8080
timeout: 5
- name: Verify Nginx responds
ansible.builtin.uri:
url: "http://localhost:8080/"
status_code: 200
register: response
- name: Check config file content
ansible.builtin.lineinfile:
path: /etc/nginx/sites-available/default
line: " server_name test.example.com;"
check_mode: true
register: config
failed_when: config.changed
prepare.yml — Pre-Test Setup
# molecule/default/prepare.yml
---
- name: Prepare
hosts: all
become: true
tasks:
- name: Install Python (needed for Ansible)
ansible.builtin.raw: apt-get update && apt-get install -y python3
when: ansible_os_family == "Debian"
changed_when: false
- name: Install prerequisites
ansible.builtin.package:
name:
- iproute2
- curl
state: present
Running Molecule
# Full test cycle: create → prepare → converge → verify → destroy
molecule test
# Individual steps
molecule create # Create instances
molecule prepare # Run prepare.yml
molecule converge # Run the role (converge.yml)
molecule verify # Run assertions (verify.yml)
molecule login # SSH into instance for debugging
molecule destroy # Tear down instances
# Run with specific scenario
molecule test -s my_scenario
# Keep instances on failure (for debugging)
molecule test --destroy=never
# Run idempotence check
molecule converge
molecule idempotence # Re-run and check for changes
Multiple Scenarios
# Create additional scenarios
molecule init scenario multi-os --driver-name docker
# molecule/multi-os/molecule.yml
---
platforms:
- name: ubuntu-2404
image: ubuntu:24.04
pre_build_image: true
- name: rocky-9
image: rockylinux:9
pre_build_image: true
- name: debian-12
image: debian:12
pre_build_image: true
CI/CD Integration
# .github/workflows/molecule.yml
name: Molecule Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
scenario:
- default
- multi-os
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install molecule molecule-plugins[docker] ansible-lint
- name: Run Molecule
run: molecule test -s ${{ matrix.scenario }}
env:
MOLECULE_DISTRO: ubuntu:24.04
Testinfra Verifier (Alternative)
# molecule/default/tests/test_default.py
import pytest
def test_nginx_installed(host):
nginx = host.package("nginx")
assert nginx.is_installed
def test_nginx_running(host):
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_listening(host):
socket = host.socket("tcp://0.0.0.0:8080")
assert socket.is_listening
def test_config_file(host):
config = host.file("/etc/nginx/sites-available/default")
assert config.exists
assert config.contains("server_name test.example.com")
Troubleshooting
| Issue | Solution |
|---|---|
| Docker permission denied | Add user to docker group or use sudo |
| Container won't start | Use privileged: true and proper command |
| Python not found | Add prepare step to install python3 |
| Idempotence fails | Check for tasks that always report changed |
| Slow tests | Use pre_build_image: true to skip building |
Best Practices
- Test idempotence — run converge twice, second should have 0 changes
- Test multiple OS — Ubuntu + Rocky/RHEL at minimum
- Keep tests fast — use pre-built images, minimize prepare steps
- Test in CI — run Molecule on every PR
- Verify behavior, not implementation — check ports, services, responses — not file contents
- Use
check_modein verify — confirm desired state without changing it
Conclusion
Molecule transforms Ansible role development from "push and pray" to test-driven automation. Every role should have Molecule tests covering installation, configuration, service state, and idempotence. Combined with CI/CD, you catch breaking changes before they reach production.