Introduction

Molecule is the standard testing framework for Ansible roles. It creates ephemeral instances (Docker, Podman, Vagrant, cloud), runs your role, verifies the result, and tears everything down. Essential for CI/CD and ensuring roles work before production deployment.

Install Molecule

pip install molecule molecule-docker

# Or with Podman
pip install molecule molecule-podman

# Verify
molecule --version

Initialize a Role with Molecule

# Create new role with Molecule scaffold
molecule init role myorg.myapp

# Or add Molecule to existing role
cd roles/myapp
molecule init scenario -d docker

Directory structure:

roles/myapp/
├── defaults/main.yml
├── handlers/main.yml
├── meta/main.yml
├── molecule/
│   └── default/
│       ├── molecule.yml        # Configuration
│       ├── converge.yml        # Playbook to test
│       ├── verify.yml          # Verification tests
│       └── prepare.yml         # Pre-test setup (optional)
├── tasks/main.yml
└── templates/

molecule.yml Configuration

# molecule/default/molecule.yml
---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: ubuntu-test
    image: ubuntu:22.04
    pre_build_image: true
    command: /bin/bash
    tmpfs:
      - /run
      - /tmp

  - name: rocky-test
    image: rockylinux:9
    pre_build_image: true
    command: /bin/bash
    tmpfs:
      - /run
      - /tmp

provisioner:
  name: ansible
  config_options:
    defaults:
      gathering: smart
  inventory:
    group_vars:
      all:
        app_port: 8080
        app_env: test

verifier:
  name: ansible

converge.yml — The Test Playbook

# molecule/default/converge.yml
---
- name: Converge
  hosts: all
  become: true
  tasks:
    - name: Include myapp role
      ansible.builtin.include_role:
        name: myapp

verify.yml — Verification Tests

# molecule/default/verify.yml
---
- name: Verify
  hosts: all
  become: true
  tasks:
    - name: Check package is installed
      ansible.builtin.package:
        name: nginx
        state: present
      check_mode: true
      register: pkg
      failed_when: pkg.changed

    - name: Check service is running
      ansible.builtin.service:
        name: nginx
        state: started
      check_mode: true
      register: svc
      failed_when: svc.changed

    - name: Check config file exists
      ansible.builtin.stat:
        path: /etc/nginx/nginx.conf
      register: conf
      failed_when: not conf.stat.exists

    - name: Check port is listening
      ansible.builtin.wait_for:
        port: 80
        timeout: 5

    - name: Check config content
      ansible.builtin.command: grep "worker_processes" /etc/nginx/nginx.conf
      changed_when: false

Run Tests

# Full test sequence (create → converge → idempotence → verify → destroy)
molecule test

# Step by step
molecule create        # Create instances
molecule converge      # Run playbook
molecule idempotence   # Run again (should have 0 changes)
molecule verify        # Run verification
molecule destroy       # Clean up

# Lint only
molecule lint

# Login to test instance
molecule login --host ubuntu-test

# List instances
molecule list

Test Sequence

The default molecule test runs:

  1. dependency — Install Galaxy requirements
  2. lint — Lint playbooks (yamllint, ansible-lint)
  3. cleanup — Pre-test cleanup
  4. destroy — Remove old instances
  5. syntax — Check syntax
  6. create — Create test instances
  7. prepare — Run prepare.yml (pre-test setup)
  8. converge — Run converge.yml (your role)
  9. idempotence — Run converge again (zero changes expected)
  10. verify — Run verify.yml (assertions)
  11. cleanup — Post-test cleanup
  12. destroy — Remove instances

prepare.yml — Pre-Test Setup

# molecule/default/prepare.yml
---
- name: Prepare
  hosts: all
  become: true
  tasks:
    - name: Install Python (for minimal images)
      ansible.builtin.raw: apt-get update && apt-get install -y python3
      changed_when: false
      when: ansible_os_family == 'Debian'

    - name: Install prerequisites
      ansible.builtin.package:
        name:
          - sudo
          - systemd
        state: present

Multiple Scenarios

molecule/
├── default/           # Default scenario (Docker)
│   ├── molecule.yml
│   ├── converge.yml
│   └── verify.yml
├── podman/            # Podman scenario
│   ├── molecule.yml
│   ├── converge.yml
│   └── verify.yml
└── vagrant/           # Vagrant scenario (full VM)
    ├── molecule.yml
    ├── converge.yml
    └── verify.yml
# Run specific scenario
molecule test -s podman
molecule test -s vagrant

CI/CD Integration

GitHub Actions

# .github/workflows/molecule.yml
name: Molecule Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install molecule molecule-docker ansible-lint
      - name: Run Molecule
        run: molecule test

GitLab CI

# .gitlab-ci.yml
molecule:
  image: python:3.11
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2375
  before_script:
    - pip install molecule molecule-docker
  script:
    - molecule test

Testing with Ansible Verify

# verify.yml — Comprehensive verification
---
- name: Verify
  hosts: all
  become: true
  tasks:
    # Check packages
    - name: Verify packages installed
      ansible.builtin.package:
        name: "{{ item }}"
        state: present
      check_mode: true
      register: pkg_check
      failed_when: pkg_check.changed
      loop:
        - nginx
        - redis-server

    # Check files
    - name: Verify config files
      ansible.builtin.stat:
        path: "{{ item }}"
      register: file_check
      failed_when: not file_check.stat.exists
      loop:
        - /etc/nginx/nginx.conf
        - /etc/nginx/sites-enabled/myapp

    # Check services
    - name: Verify services running
      ansible.builtin.command: systemctl is-active {{ item }}
      changed_when: false
      loop:
        - nginx
        - redis-server

    # Check connectivity
    - name: Verify HTTP response
      ansible.builtin.uri:
        url: http://localhost:80
        status_code: 200

    # Check file content
    - name: Verify config content
      ansible.builtin.lineinfile:
        path: /etc/nginx/nginx.conf
        line: "worker_processes auto;"
      check_mode: true
      register: content_check
      failed_when: content_check.changed

Troubleshooting

Docker Permission Denied

# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker

systemd Not Available in Container

Use command: /sbin/init and privileged mode:

platforms:
  - name: test
    image: ubuntu:22.04
    command: /sbin/init
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
    cgroupns_mode: host

Conclusion

Molecule is the standard for testing Ansible roles. It creates disposable instances, runs your role, checks idempotency (zero changes on second run), and runs verification tests. Use Docker for fast local testing and integrate with GitHub Actions or GitLab CI for automated testing on every commit. The key workflow: molecule test runs the full sequence — create, converge, idempotence check, verify, destroy. Write thorough verify.yml tests that check packages, services, files, and connectivity.