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

IssueSolution
Docker permission deniedAdd user to docker group or use sudo
Container won't startUse privileged: true and proper command
Python not foundAdd prepare step to install python3
Idempotence failsCheck for tasks that always report changed
Slow testsUse pre_build_image: true to skip building

Best Practices

  1. Test idempotence — run converge twice, second should have 0 changes
  2. Test multiple OS — Ubuntu + Rocky/RHEL at minimum
  3. Keep tests fast — use pre-built images, minimize prepare steps
  4. Test in CI — run Molecule on every PR
  5. Verify behavior, not implementation — check ports, services, responses — not file contents
  6. Use check_mode in 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.